# State Management Standards for Kotlin Android
This document outlines the coding standards and best practices for managing application state in Kotlin Android projects. Proper state management is crucial for maintainability, testability, performance, and overall application architecture. It emphasizes modern approaches using Kotlin's features and the Android Jetpack libraries. This guide is designed for Kotlin Android developers and serves as a reference for AI coding assistants.
## 1. Guiding Principles
* **Single Source of Truth:** Every piece of state should exist in only one place in the application. This prevents inconsistencies and simplifies debugging.
* **Unidirectional Data Flow:** Data should flow in a single direction through the application. This makes the flow predictable and easier to reason about.
* **Immutability:** Prefer immutable data structures. This reduces the risk of accidental state changes and facilitates concurrent access.
* **Explicit State:** UI components should declare their state explicitly rather than relying on implicit or hidden state.
* **Separation of Concerns:** Decouple UI code from business logic and state management. This improves testability and maintainability.
## 2. Architectural Patterns for State Management
### 2.1. Model-View-Intent (MVI)
MVI is a reactive architectural pattern that enforces a unidirectional data flow.
* **Model:** Represents the immutable state of the UI.
* **View:** Renders the UI based on the current state. Observes state, and emits intents.
* **Intent:** Represents the user's intention to perform an action.
* **Reducer:** Pure function which modifies the state based on intent and previous state.
* **Effect:** Side Effect which reacts to reducer changes.
**Do This:**
* Use MVI when building complex UIs with a lot of dynamic state.
* Employ libraries like Turbine for testing.
* Consider Coroutines Flow for efficient state updates.
**Don't Do This:**
* Don't mutate the state directly inside the View.
* Don't perform side effects within the reducer.
**Code Example:**
"""kotlin
import kotlinx.coroutines.flow.*
// 1. Define the state
data class MainState(
val isLoading: Boolean = false,
val data: String? = null,
val error: String? = null
)
// 2. Define the intent
sealed class MainIntent {
object LoadData : MainIntent()
data class UpdateData(val newData: String) : MainIntent()
}
// 3. Define the effect
sealed class MainEffect {
data class ShowError(val message: String) : MainEffect()
}
// 4. The ViewModel holding the state and handling intents
class MainViewModel {
private val _state = MutableStateFlow(MainState())
val state: StateFlow = _state.asStateFlow() //Expose for the view to render
private val _effect: MutableSharedFlow = MutableSharedFlow()
val effect = _effect.asSharedFlow() //Expose for the view to react
fun processIntent(intent: MainIntent) {
when (intent) {
MainIntent.LoadData -> loadData()
is MainIntent.UpdateData -> updateData(intent.newData)
}
}
private fun loadData() {
_state.update { it.copy(isLoading = true, error = null) }
// Simulate loading data from a repository
viewModelScope.launch {
try {
//Simulate IO.
delay(1000)
_state.update { it.copy(isLoading = false, data = "Loaded data") }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.message) }
_effect.emit(MainEffect.ShowError("Failed to load data"))
}
}
}
private fun updateData(newData: String) {
_state.update { it.copy(data = newData) }
}
}
// 5. The View (Activity/Fragment)
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) { //Collect the ViewEffect
viewModel.effect.collect{ effect ->
when(effect) {
is MainEffect.ShowError -> {
//Show error message via Toast or Compose Snackbar
Toast.makeText(this@MainActivity, effect.message, Toast.LENGTH_SHORT).show()
}
}
}
}
Column {
if (state.isLoading) {
Text("Loading...")
} else if (state.error != null) {
Text("Error: ${state.error}")
} else {
Text("Data: ${state.data ?: "No data"}")
Button(onClick = { viewModel.processIntent(MainIntent.LoadData) }) {
Text("Load Data")
}
Button(onClick = { viewModel.processIntent(MainIntent.UpdateData("New Data")) }) {
Text("Update Data")
}
}
}
}
}
}
"""
**Why:**
* MVI promotes a clear separation of concerns and predictable state management, making the app more maintainable and testable. The explicit "Effect" makes side effects predictable.
**Anti-Pattern:**
* Integrating side effects (e.g., network calls, database updates) directly into the View or Model makes the app harder to test and maintain. Instead, side effects should be routed and captured within the Effect.
### 2.2. Model-View-ViewModel (MVVM) with StateFlow/LiveData
MVVM is an architectural pattern that separates the UI (View) from the data and logic (ViewModel).
* **Model:** Represents the data layer and business logic.
* **View:** Displays the data and forwards user actions to the ViewModel.
* **ViewModel:** Exposes data streams for the View to observe and handles user actions by interacting with the Model. It holds the *state*.
**Do This:**
* Use MVVM as the standard architecture for building most UIs.
* Use "StateFlow" for complex state that benefits from reactive updates and "LiveData" for simpler scenarios or when interoperability with older code is necessary. Use "SharedFlow" for passing one-off events.
* Use Coroutines to perform asynchronous operations in the ViewModel.
**Don't Do This:**
* Don't put UI logic in the ViewModel.
* Don't reference "Context" or "View" instances in the ViewModel.
**Code Example:**
"""kotlin
import androidx.lifecycle.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
// 1. Define the state
data class UserState(
val isLoading: Boolean = false,
val userName: String? = null,
val errorMessage: String? = null
)
// 2. The ViewModel holding the state
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _userState = MutableStateFlow(UserState())
val userState: StateFlow = _userState.asStateFlow()
init {
loadUser()
}
fun loadUser() {
viewModelScope.launch {
_userState.update { it.copy(isLoading = true, errorMessage = null) }
try {
val user = userRepository.getUser()
_userState.update { it.copy(isLoading = false, userName = user.name) }
} catch (e: Exception) {
_userState.update { it.copy(isLoading = false, errorMessage = e.message) }
}
}
}
}
// 3. The Repository (Model)
class UserRepository {
// Simulate network call
suspend fun getUser(): User {
delay(1000) // Simulate network delay
return User("John Doe")
}
}
data class User(val name: String)
// 4. The View (Activity/Fragment)
class UserActivity : ComponentActivity() {
private val userViewModel: UserViewModel by viewModels {
UserViewModelFactory((application as YourApplication).userRepository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by userViewModel.userState.collectAsState()
Column {
if (state.isLoading) {
Text("Loading...")
} else if (state.errorMessage != null) {
Text("Error: ${state.errorMessage}")
} else {
Text("User: ${state.userName ?: "No user"}")
}
}
}
}
}
class UserViewModelFactory(private val userRepository: UserRepository) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserViewModel(userRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
"""
**Why:**
* MVVM separates UI logic from business logic, making the app more testable and maintainable. StateFlow/LiveData provide a reactive way to update the UI when the state changes.
* ViewModel survives configuration changes, preventing data loss.
**Anti-Pattern:**
* Putting business logic in the Activity/Fragment makes the app harder to test and maintain.
### 2.3. Unidirectional Data Flow with Jetpack Compose
Jetpack Compose encourages a unidirectional data flow where UI components are functions of state.
**Do This:**
* Use "remember" to hold state within composables.
* Use "MutableState" or "mutableStateOf" to create observable state.
* Use "LaunchedEffect" or "rememberCoroutineScope" to perform side effects. Handle UI events with simple callbacks.
**Don't Do This:**
* Don't modify state directly within the composable without using "remember" and "mutableStateOf".
* Don't perform complex business logic inside composables.
**Code Example:**
"""kotlin
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material.*
import androidx.compose.foundation.layout.*
@Composable
fun CounterApp() {
Column {
// 1. Define and hold the state
var count by remember { mutableStateOf(0) }
// 2. Display the state
Text(text = "Count: $count")
// 3. Allow users to modify the state via UI events
Row {
Button(onClick = { count++ }) {
Text("Increment")
}
Button(onClick = { count-- }) {
Text("Decrement")
}
}
}
}
@Preview
@Composable
fun PreviewCounterApp() {
CounterApp()
}
"""
**Why:**
* Compose promotes a declarative UI paradigm, making it easier to reason about and maintain the UI. Immutable data structures greatly improve testability within Composables.
* Unidirectional data flow simplifies state management and reduces the risk of unexpected side effects.
**Anti-Pattern:**
* Modifying external state directly within a composable makes the UI harder to reason about and test.
### 2.4. Using Redux with Kotlin
Redux is a state management pattern often used in complex applications. It is inspired by the Elm Architecture. It features a single store and pure reducers for processing state.
**Do This:**
* Consider Redux for apps requiring complex state management and time-travel debugging or state persistence.
**Don't Do This:**
* Avoid overusing Redux for simple apps, as it can add unnecessary complexity.
**Code Example:**
"""kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import androidx.lifecycle.*
// 1. Define the state
data class AppState(val count: Int = 0)
// 2. Define the actions
sealed class AppAction {
object Increment : AppAction()
object Decrement : AppAction()
}
// 3. Define the reducer
fun reducer(state: AppState, action: AppAction): AppState {
return when (action) {
AppAction.Increment -> state.copy(count = state.count + 1)
AppAction.Decrement -> state.copy(count = state.count - 1)
}
}
// 4. Define the store
class Store(initialState: AppState) {
private val _state = MutableStateFlow(initialState)
val state: StateFlow = _state.asStateFlow()
fun dispatch(action: AppAction) {
_state.value = reducer(_state.value, action)
}
}
// 5. ViewModel usage:
class MyViewModel : ViewModel() {
private val store = Store(AppState())
val state: StateFlow = store.state
fun increment() {
store.dispatch(AppAction.Increment)
}
fun decrement() {
store.dispatch(AppAction.Decrement)
}
}
// 6. Activity/Fragment Usage
class ReduxActivity : ComponentActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by viewModel.state.collectAsState()
Column {
Text("Count: ${state.count}")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
Button(onClick = { viewModel.decrement() }) {
Text("Decrement")
}
}
}
}
}
"""
**Why:**
* Redux enforces a strict unidirectional data flow and makes state management predictable
* Redux simplifies debugging by providing a single source of truth.
**Anti-Pattern:**
* Using Redux for simple state makes the application overly complex.
* Mutating state directly in the reducer breaks the immutability principle.
## 3. Technology-Specific Details
### 3.1. Handling Configuration Changes
* **ViewModel:** Use "ViewModel" to retain data across configuration changes.
* **"rememberSaveable" in Compose:** Use "rememberSaveable" in Compose to save and restore state across configuration changes.
**Code Example:**
"""kotlin
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.material.Text
@Composable
fun MyComposable() {
var myValue by rememberSaveable { mutableStateOf("Initial Value") }
Text(text = "My Value: $myValue")
// ...
}
"""
### 3.2. Saving UI State
* **"onSaveInstanceState()":** Use "onSaveInstanceState()" in Activities/Fragments to save UI state when the app is backgrounded.
* **"rememberSavable()":** Utilize "rememberSavable" to automatically save state across activity recreation.
**Code Example:**
"""kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
class SavingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
MyComposable()
}
}
}
}
"""
### 3.3. Use of "SavedStateHandle"
* Instantiate your "ViewModel" with the "SavedStateHandle" for state persistence across process death scenarios:
"""kotlin
import androidx.lifecycle.*
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private val _myState = MutableLiveData()
val myState: LiveData = _myState
init {
// Retrieve the saved state, or use a default value
_myState.value = savedStateHandle.get("my_state_key") ?: "Default Value"
}
fun updateState(newValue: String) {
_myState.value = newValue
// Save the state
savedStateHandle.set("my_state_key", newValue)
}
}
"""
**Why:**
* "SavedStateHandle" offers a robust solution to manage state restoration during process death, providing data that survives beyond configuration changes, ensuring a reliable user experience.
### 3.4 Jetpack DataStore
Use Jetpack DataStore instead of shared preferences for storing key-value pairs or typed objects.
DataStore offers coroutines and Flow support, transactional APIs, and strong consistency.
"""kotlin
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// Create DataStore
val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
// Example preferences keys
object StoreKeys {
val exampleCounter = intPreferencesKey("example_counter")
}
// Read the value
val exampleCounterFlow: Flow = context.dataStore.data
.map { preferences ->
preferences[StoreKeys.exampleCounter] ?: 0
}
// Update the value
suspend fun incrementCounter(context: Context) {
context.dataStore.edit { preferences ->
val currentCounterValue = preferences[StoreKeys.exampleCounter] ?: 0
preferences[StoreKeys.exampleCounter] = currentCounterValue + 1
}
}
"""
**Why:**
* DataStore provides a modern and type-safe way to persist data, offering improved performance and consistency compared to SharedPreferences. It also works seamlessly with Kotlin Coroutines and Flow.
## 4. Core Kotlin Features
### 4.1. Immutability
* Use "val" for immutable variables.
* Use data classes for immutable data structures.
* Use "copy()" method to create modified copies of data classes.
**Code Example:**
"""kotlin
data class Person(val name: String, val age: Int)
fun main() {
val person1 = Person("Alice", 30)
val person2 = person1.copy(age = 31) // Create a new instance with a modified property
println(person1)
println(person2)
}
"""
### 4.2. Coroutines
* Use Coroutines for asynchronous operations.
* Use "viewModelScope" for launching Coroutines in ViewModels.
* Use "lifecycleScope" for launching Coroutines in Activities/Fragments.
**Code Example:**
"""kotlin
import androidx.lifecycle.*
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
// Perform asynchronous operation
val result = performNetworkRequest()
// Update UI
}
}
suspend fun performNetworkRequest(): String {
delay(1000) // Simulate network delay
return "Data from network"
}
}
"""
### 4.3. Kotlin Flow
* Use "StateFlow" for observable state holders that emit current state and updates.
* Use "SharedFlow" for emitting events or one-off updates.
* Use "collectAsState()" in Compose to collect Flow values.
**Code Example:**
"""kotlin
import androidx.compose.runtime.*
import kotlinx.coroutines.flow.*
class MyViewModel : ViewModel() {
private val _myState = MutableStateFlow("Initial Value")
val myState: StateFlow = _myState.asStateFlow()
fun updateState(newValue: String) {
_myState.value = newValue
}
}
@Composable
fun MyComposable(viewModel: MyViewModel) {
val stateValue by viewModel.myState.collectAsState()
Text(text = "State Value: $stateValue")
}
"""
## 5. Testing State Management
### 5.1. Unit Tests
* Write unit tests for ViewModels, reducers, and other state management components.
* Mock dependencies to isolate units of code.
* Use "Turbine" library with Flow to collect and verify values:
"""kotlin
import kotlinx.coroutines.test.runTest
import app.cash.turbine.test
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.test.Test
import kotlin.test.assertEquals
class ViewModelTest {
@Test
fun "test state updates"() = runTest {
val viewModel = MyViewModel()
val stateFlow = MutableStateFlow("initial")
stateFlow.test {
assertEquals("initial", awaitItem())
stateFlow.emit("new")
assertEquals("new", awaitItem())
cancelAndConsumeRemainingEvents()
}
}
}
"""
### 5.2. UI Tests
* Write UI tests to verify the behavior of the UI when state changes.
* Use "ComposeTestRule" in Compose to interact with UI elements.
"""kotlin
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
class ComposeUITest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testIncrementButton() {
composeTestRule.setContent {
CounterApp()
}
composeTestRule.onNodeWithText("Increment").performClick()
composeTestRule.onNodeWithText("Count: 1").assertExists()
}
}
"""
## 6. Performance Considerations
* Avoid unnecessary state updates. Only update the state when it actually changes.
* Use immutable data structures to prevent accidental state changes.
* Use "collectAsStateWithLifecycle()" for collecting "StateFlow" in compose when running in the ui layer. This prevents the state from being collected when the UI is not visible.
* Use appropriate data structures for storing state (e.g., "ImmutableList", "ImmutableMap").
## 7. Security Considerations
* Protect sensitive data by encrypting it before storing it.
* Avoid storing sensitive data in UI state.
## 8. Deprecated Features / Known Issues
* Be aware that "LiveData" lacks some of the advanced features offered by Kotlin Flows (e.g., complex transformations, backpressure handling). Consider migrating "LiveData" to "StateFlow".
* Be mindful of potential memory leaks when using "rememberCoroutineScope" within a Composable. Ensure that the scope is properly tied to the lifecycle of the Composable. Ensure that LaunchedEffect keys are only triggered when changes are truly needed.
This comprehensive guide provides a strong foundation for managing state in Kotlin Android applications and aims to assist developers in creating robust, maintainable, and performant applications. Keeping abreast of the latest advancements and best practices will be critical in the evolution of Kotlin Android development.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for Kotlin Android This document outlines the core architectural standards for Kotlin Android development. It aims to guide developers in creating maintainable, scalable, and robust applications by establishing best practices for project structure, architectural patterns, and organization principles. ## 1. Architectural Patterns Choosing the right architectural pattern is crucial for structuring your Android application. This section outlines the recommended architectural patterns and their implementation in Kotlin. ### 1.1 Recommended Architecture: MVVM (Model-View-ViewModel) MVVM is the recommended architectural pattern for Kotlin Android applications due to its clear separation of concerns, testability, and maintainability. It separates the UI (View), the data (Model), and the logic that connects them (ViewModel). **Do This:** * Employ MVVM architecture for all new Android projects and refactor existing projects to adopt MVVM where feasible. * Ensure a clear separation between the View (Activities/Fragments), ViewModel, and Model layers. * Use data binding to connect the View and ViewModel, reducing boilerplate code. * Employ unidirectional Data Flow **Don't Do This:** * Directly manipulate the UI from the Model layer. * Place business logic within Activities or Fragments. * Treat Activities/Fragments as more than just UI controllers. **Why This Matters:** MVVM enhances code maintainability, testability (ViewModels can be easily unit-tested), and reusability. It also facilitates parallel development by separating UI design from business logic implementation. **Code Example:** """kotlin // Model: Data class representing a user data class User(val name: String, val age: Int) // ViewModel: Exposes user data to the View and handles user interactions class UserViewModel : ViewModel() { private val _user = MutableLiveData<User>() val user: LiveData<User> = _user init { // Initialize with some data _user.value = User("John Doe", 30) } fun updateUser(name: String, age: Int) { _user.value = User(name, age) } } // View: Activity or Fragment observes the ViewModel and updates the UI class UserActivity : AppCompatActivity() { private lateinit var binding: ActivityUserBinding private val userViewModel: UserViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityUserBinding.inflate(layoutInflater) setContentView(binding.root) userViewModel.user.observe(this) { user -> binding.nameTextView.text = user.name binding.ageTextView.text = user.age.toString() } binding.updateButton.setOnClickListener { userViewModel.updateUser("Jane Doe", 25) } } } // activity_user.xml <layout xmlns:android="http://schemas.android.com/apk/res/android"> <LinearLayout ...> <TextView android:id="@+id/nameTextView" .../> <TextView android:id="@+id/ageTextView" .../> <Button android:id="@+id/updateButton" .../> </LinearLayout> </layout> """ **Anti-Pattern:** Placing business logic directly inside the activity. """kotlin // Anti-pattern: Business logic in activity class BadUserActivity : AppCompatActivity() { private lateinit var binding: ActivityUserBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityUserBinding.inflate(layoutInflater) setContentView(binding.root) binding.updateButton.setOnClickListener { // Directly manipulating UI and data here - BAD! binding.nameTextView.text = "Jane Doe" binding.ageTextView.text = "25" } } } """ ### 1.2 Alternatives: MVI (Model-View-Intent) MVI is another architecture, especially suitable when complex state management is needed but the complexity added warrants its use. **Do This:** * Embrace unidirectional data flow: "View -> Intent -> Model -> State -> View" * Implement immutable state * Use Kotlin coroutines or RxJava for handling asynchronous operations and state updates **Don't Do This:** * Mutate the state directly; always create a new state object. * Overcomplicate simple applications with MVI where MVVM suffice **Why This Matters:** MVI offers predictable state management, ease of debugging, and better testability. The unidirectional flow makes it easy to track changes and understand application behavior. **Code Example:** """kotlin // Data classes for State, Intent, and Result data class UserState(val name: String = "", val age: Int = 0) sealed class UserIntent { data class UpdateName(val name: String) : UserIntent() data class UpdateAge(val age: Int) : UserIntent() } sealed class UserResult { data class NameUpdated(val name: String) : UserResult() data class AgeUpdated(val age: Int) : UserResult() } // ViewModel acting as the "Controller" class UserViewModel : ViewModel() { private val _state = MutableStateFlow(UserState()) val state: StateFlow = _state.asStateFlow() fun processIntent(intent: UserIntent) { when (intent) { is UserIntent.UpdateName -> updateName(intent.name) is UserIntent.UpdateAge -> updateAge(intent.age) } } private fun updateName(name: String) { _state.value = _state.value.copy(name = name) } private fun updateAge(age: Int) { _state.value = _state.value.copy(age = age) } } // View (Activity/Fragment) class UserActivity : AppCompatActivity() { private lateinit var binding: ActivityUserBinding private val userViewModel: UserViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityUserBinding.inflate(layoutInflater) setContentView(binding.root) lifecycleScope.launch { userViewModel.state.collect { state -> binding.nameTextView.text = state.name binding.ageTextView.text = state.age.toString() } } binding.updateNameButton.setOnClickListener { userViewModel.processIntent(UserIntent.UpdateName("Jane Doe")) } binding.updateAgeButton.setOnClickListener { userViewModel.processIntent(UserIntent.UpdateAge(25)) } } } """ ### 1.3 Alternatives: Clean Architecture Clean Architecture is a software design philosophy. It's mostly platform agnostic but can be adapted to Android development. The idea is to isolate the business rules, making the system easier to test, maintain, and extend. **Do This:** * Separate entities, use cases, interface adapters, and frameworks/drivers. * Define clear boundaries between layers using interfaces. * Make dependency flow towards the entities (business logic). **Don't Do This:** * Allow UI concerns to dictate business logic. * Tightly couple your business logic to Android framework components. **Why This Matters:** Clean Architecture focuses on isolating core business logic enabling independent evolution of core application logic from UI and data layer. This provides scalability and maintainability, even as underlying frameworks change over time. **Code Example:** """kotlin // Entity Layer data class Product(val id: Int, val name: String, val price: Double) // Use Case Layer (Interactor) class GetProductUseCase(private val productRepository: ProductRepository) { suspend fun execute(productId: Int): Product { return productRepository.getProduct(productId) } } // Interface Adapters Layer (Repository Interface) interface ProductRepository { suspend fun getProduct(productId: Int): Product } // Frameworks & Drivers Layer (Repository Implementation) class ProductRepositoryImpl(private val productApi: ProductApi) : ProductRepository { override suspend fun getProduct(productId: Int): Product { // Implementation using Retrofit, for example return productApi.getProduct(productId) } } // API Interface (using Retrofit) interface ProductApi { @GET("products/{id}") suspend fun getProduct(@Path("id") productId: Int): Product } // ViewModel using the Use Case class ProductViewModel(private val getProductUseCase: GetProductUseCase) : ViewModel() { private val _product = MutableLiveData<Product>() val product: LiveData<Product> = _product fun fetchProduct(productId: Int) { viewModelScope.launch { try { _product.value = getProductUseCase.execute(productId) } catch (e: Exception) { // Handle error } } } } """ ## 2. Project Structure A well-defined project structure is essential for code organization and maintainability. ### 2.1 Package Structure **Do This:** * Organize your code into feature-based packages. For example, "com.example.app.feature_name". * Create separate packages for models, views, viewmodels, repositories, and utilities. * Use "internal" visibility modifier wherever possible to limit access to classes/functions in a package **Don't Do This:** * Place all classes in a single package. * Mix UI-related components with business logic in the same package. **Why This Matters:** A clear package structure promotes code discoverability, reduces dependencies, and improves maintainability. It allows developers to quickly locate and understand different parts of the application. **Code Example:** """ com.example.app ├── feature_auth │ ├── ui │ │ ├── AuthActivity.kt │ │ └── AuthViewModel.kt │ ├── data │ │ ├── AuthRepository.kt │ │ └── AuthApiService.kt │ └── model │ └── User.kt ├── feature_home │ ├── ui │ │ ├── HomeActivity.kt │ │ └── HomeViewModel.kt │ ├── data │ │ ├── HomeRepository.kt │ │ └── HomeApiService.kt │ └── model │ └── Article.kt ├── di │ └── AppModule.kt └── util └── NetworkUtils.kt """ ### 2.2 Module Structure For larger applications, consider using a modular architecture. Modularization is a development practice that involves dividing an application into smaller, independent modules that can be developed, tested, and deployed separately. This enhances code reusability, improves build times, and supports parallel development efforts. **Do This:** * Divide your application into modules based on features or functionalities. * Define clear module dependencies. * Each module should be independently testable * Create "api" and "implementation" configurations in each module's "build.gradle.kts". Anything exposed in "api" is accessible to external modules, "implementation" is not. Implementation details should be behind interfaces. **Don't Do This:** * Create circular module dependencies. * Over-modularize small applications, adding unnecessary complexity. * Include implementation details in API modules **Why This Matters:** Modularization improves build times, enhances code reusability, and supports parallel development efforts. Modules can be independently tested and deployed, minimizing the impact of changes. **Code Example:** In "settings.gradle.kts": """kotlin include(":app") include(":feature_auth") include(":feature_home") include(":core") // Core functionalities """ In "feature_auth/build.gradle.kts": """kotlin dependencies { implementation(project(":core")) // Other dependencies } """ In "feature_home/build.gradle.kts": """kotlin dependencies { implementation(project(":core")) implementation(project(":feature_auth")) //Can depend on other feature modules // Other dependencies } """ ## 3. Data Management Efficiently managing data is crucial for Android applications. ### 3.1 Repository Pattern **Do This:** * Implement a repository layer to abstract data sources (local database, network API, etc.). * Use Kotlin Coroutines or RxJava for handling asynchronous operations in the repository. Expose Flow or LiveData to ViewModel **Don't Do This:** * Directly access data sources from ViewModels or UI components. * Mix data retrieval logic with business logic. **Why This Matters:** The repository pattern decouples data access logic from the rest of the application, making it easier to switch between data sources or implement caching strategies. **Code Example:** """kotlin // Data source interface interface UserDataSource { suspend fun getUser(userId: Int): User } // Remote data source (e.g., using Retrofit) class RemoteUserDataSource(private val apiService: UserApiService) : UserDataSource { override suspend fun getUser(userId: Int): User { return apiService.getUser(userId) } } // Local data source (e.g., using Room) class LocalUserDataSource(private val userDao: UserDao) : UserDataSource { override suspend fun getUser(userId: Int): User { return userDao.getUser(userId) } } // Repository implementation class UserRepository( private val remoteDataSource: RemoteUserDataSource, private val localDataSource: LocalUserDataSource ) { suspend fun getUser(userId: Int): User { // Implement caching strategy here try { val user = remoteDataSource.getUser(userId) localDataSource.userDao.insert(user) // Save to local DB return user } catch (e: Exception) { // If network fails, get from local DB return localDataSource.getUser(userId) ?: throw e } } } """ ### 3.2 Room Persistence Library **Do This:** * Use Room for local data persistence. * Define clear data entities, DAOs, and database classes. * Use Kotlin Coroutines for performing database operations asynchronously. * Implement migrations to handle schema changes with database versions **Don't Do This:** * Perform database operations on the main thread. * Store sensitive data in plain text. **Why This Matters:** Room provides a robust and type-safe way to interact with SQLite databases. It simplifies database management and reduces boilerplate code. **Code Example:** """kotlin // Entity @Entity(tableName = "users") data class User( @PrimaryKey val id: Int, val name: String, val email: String ) // DAO (Data Access Object) @Dao interface UserDao { @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUser(userId: Int): User? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(user: User) } // Database @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ).build() INSTANCE = instance instance } } } } """ ## 4. Asynchronous Operations Handling asynchronous operations correctly is vital for maintaining UI responsiveness and preventing ANR (Application Not Responding) errors. ### 4.1 Kotlin Coroutines **Do This:** * Use Kotlin Coroutines for asynchronous programming. * Use "viewModelScope" (or "lifecycleScope" where appropriate) for launching coroutines in ViewModels. * Handle exceptions properly within coroutines using "try-catch" blocks or "CoroutineExceptionHandler". **Don't Do This:** * Block the main thread with long-running operations. * Forget to cancel coroutines when they are no longer needed. * Ignore exceptions within coroutines. **Why This Matters:** Kotlin Coroutines provide a lightweight and efficient way to handle asynchronous tasks. They simplify asynchronous code and make it more readable. **Code Example:** """kotlin class MyViewModel : ViewModel() { private val _data = MutableLiveData<String>() val data: LiveData<String> = _data fun fetchData() { viewModelScope.launch { try { val result = withContext(Dispatchers.IO) { // Simulate a network call delay(1000) "Data from network" } _data.value = result } catch (e: Exception) { // Handle error Log.e("MyViewModel", "Error fetching data", e) } } } override fun onCleared() { super.onCleared() // Coroutines are automatically cancelled when the ViewModel is destroyed. } } """ ### 4.2 Flow **Do This:** * Use "Flow" for asynchronous streams of data. * Use operators to transform and filter data. * Collect "Flow" data in UI using "collectAsState" (in Jetpack Compose) or "observe" (in Activities/Fragments). **Don't Do This:** * Create infinite loops with "Flow". * Neglect error handling within your flows **Why This Matters:** Flow provides a reactive way to handle asynchronous data streams. It offers powerful operators for data transformation and composition. **Code Example:** """kotlin class MyViewModel : ViewModel() { private val _data = MutableStateFlow<String>("") val data: StateFlow<String> = _data.asStateFlow() init { viewModelScope.launch { try { flow { emit("Initial Data") delay(500) emit("Fetching...") delay(1000) emit("Data from Flow!") } .collect { result -> _data.value = result } } catch (e: Exception) { Log.e("MyViewModel", "Error with flow", e) } } } } """ ## 5. Dependency Injection Dependency injection (DI) is a design pattern that allows you to develop loosely coupled code. Libraries like Hilt and Koin are often utilized for DI in Kotlin Android projects. ### 5.1 Hilt **Do This:** * Use Hilt for dependency injection in Android applications. * Annotate classes with "@AndroidEntryPoint" for injection in Activities/Fragments. * Use "@InstallIn" to specify the scope of dependencies. * Use Qualifiers like "@Named" or create your own to differentiate between multiple dependencies of the same type **Don't Do This:** * Manually create and manage dependencies in your classes. * Overuse "@Singleton" scope, as this can lead to memory leaks and performance issues. **Why This Matters:** Hilt simplifies DI in Android by providing a standard way to manage dependencies. It reduces boilerplate code and improves testability. **Code Example:** """kotlin @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideApiService(): ApiService { return Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } @Provides fun provideUserRepository(apiService: ApiService): UserRepository { return UserRepositoryImpl(apiService) } } @AndroidEntryPoint class MyActivity : AppCompatActivity() { @Inject lateinit var userRepository: UserRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Use userRepository here } } """ ### 5.2 Koin (Alternative) Koin is a lightweight dependency injection framework for Kotlin **Do This:** * Define modules using "module {}" and declare dependencies using "single", "factory", or "viewModel". * Inject dependencies using "by inject()" in your classes. * In "Application" class, start Koin using "startKoin { modules(...) }" **Don't Do This:** * Overuse global state making testing more difficult. * Mix creation and injection logic within the same classes. **Why This Matters:** Koin reduces boilerplate code associated with dependency injection, facilitating clearer and more concise dependency management. **Code Example:** """kotlin val appModule = module { single { ApiService() } // Define a singleton factory { UserRepository(get()) } // Define a new instance each time viewModel { MyViewModel(get()) } // Declare a ViewModel } class MyViewModel(private val userRepository: UserRepository) : ViewModel() { // ... } class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } } class MyActivity : AppCompatActivity() { private val myViewModel: MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... } } """ ## 6. Testing Writing automated tests is essential for ensuring the quality and reliability of your application. ### 6.1 Unit Tests **Do This:** * Write unit tests for ViewModels, repositories, and utility classes. * Use mocking frameworks like Mockito or MockK to isolate units of code. * Follow the AAA (Arrange, Act, Assert) pattern for structuring tests. **Don't Do This:** * Skip unit tests for critical components. * Write tests that are tightly coupled to implementation details. * Write tests that require a running emulator/device **Why This Matters:** Unit tests verify the correctness of individual units of code. They help catch bugs early and ensure that changes do not break existing functionality. **Code Example:** """kotlin @Test fun "fetchData should update data LiveData with result"() = runBlocking { // Arrange val mockRepository = mockk<UserRepository>() val expectedResult = "Test Data" coEvery { mockRepository.getData() } returns expectedResult val viewModel = MyViewModel(mockRepository) // Act viewModel.fetchData() delay(100) // Give time for coroutine to execute // Assert assertEquals(expectedResult, viewModel.data.getOrAwaitValue()) } """ ### 6.2 UI Tests **Do This:** * Write UI tests to verify user interactions and UI behavior. * Use Espresso for writing instrumented UI tests. * Use UI Automator for testing across multiple apps. **Don't Do This:** * Write flaky or unreliable UI tests. * Test implementation details in UI tests. **Why This Matters:** UI tests verify that the UI behaves as expected and that user interactions are handled correctly. They help catch UI-related bugs and ensure a good user experience. ## 7. Code formatting and linting Maintaining a consistent code style throughout the project makes the code readable and easier to contribute to. ### 7.1. Kotlin Style Guide **Do This:** * Adhere to the official [Kotlin style guide](https://kotlinlang.org/docs/coding-conventions.html) and [Android Kotlin Style Guide](https://developer.android.com/kotlin/style-guide). * Use ".editorconfig" files to enforce consistent code style in your IDE * Use detekt for static code analysis * Configure Android Studio code style settings as per the Kotlin style guide. **Don't Do This:** * Ignore code formatting issues. * Commit code with inconsistent styling. ## 8. Security Security considerations are crucial for Android development to protect user data and prevent vulnerabilities. ### 8.1 Data Encryption **Do This:** * Encrypt sensitive data stored locally using the Android Keystore system. * Use HTTPS for network communication. * Validate user input to prevent injection attacks. **Don't Do This:** * Store passwords or API keys in plain text. * Trust user input without validation. ### 8.2 Permissions **Do This:** * Request only necessary permissions. * Explain why each permission is needed to the user. * Handle permission requests gracefully. **Don't Do This:** * Request excessive permissions. * Assume permissions are granted without checking. ## 9. Summary Following these core architecture standards will result in Kotlin Android applications that are well-structured, maintainable, testable, and secure. Remember to regularly review and update these standards as the Kotlin and Android ecosystems evolve.
# Component Design Standards for Kotlin Android This document outlines the coding standards for designing components in Kotlin Android applications. Adhering to these standards will lead to more reusable, maintainable, testable, and performant Android applications. This focuses specifically on component design principles tailored for the Kotlin Android ecosystem. ## 1. Architectural Principles ### 1.1 Layered Architecture **Standard:** Structure your application following a layered architecture (Presentation, Domain, Data). * **Do This:** Clearly separate UI-related code (Activities, Fragments, Composables) from business logic and data access concerns. Employ a Model-View-ViewModel (MVVM) or Model-View-Intent (MVI) pattern for the Presentation layer. * **Don't Do This:** Avoid tightly coupling UI elements directly to data sources or database operations. Refrain from placing business logic directly within Activity or Fragment classes. **Why:** * **Maintainability:** Changes in one layer have minimal impact on other layers. * **Testability:** Each layer can be tested independently. * **Readability:** Code is easier to understand and navigate with clear separation of concerns. * **Reusability:** Business logic and data access components can be reused across different parts of the application. **Example (MVVM):** """kotlin // Presentation Layer (ViewModel) class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _user = MutableLiveData<User>() val user: LiveData<User> = _user fun fetchUser(userId: String) { viewModelScope.launch { try { val fetchedUser = userRepository.getUser(userId) _user.value = fetchedUser } catch (e: Exception) { // Handle error } } } } // Domain Layer (Repository Interface) interface UserRepository { suspend fun getUser(userId: String): User } // Data Layer (Repository Implementation) class UserRepositoryImpl(private val userDataSource: UserDataSource) : UserRepository { override suspend fun getUser(userId: String): User { return userDataSource.getUser(userId) // From network or database } } // Data Layer (Data Source Interface) interface UserDataSource { suspend fun getUser(userId: String) : User } //Data Layer (Retrofit Implementation) class UserRemoteDataSource(private val apiService: ApiService) : UserDataSource { override suspend fun getUser(userId: String): User { return apiService.getUser(userId) } } """ ### 1.2 Dependency Injection **Standard:** Utilize dependency injection (DI) to manage component dependencies. * **Do This:** Use a DI framework like Dagger/Hilt or Koin, or implement manual dependency injection. Prefer constructor injection over field injection. * **Don't Do This:** Avoid hardcoding dependencies within classes using "new" keyword or static factories that create tight coupling and hinder testing. **Why:** * **Testability:** Dependencies can be easily mocked or stubbed during testing. * **Reusability:** Components become more reusable as they are not tied to specific implementations. * **Maintainability:** Decoupling simplifies code changes and reduces ripple effects. * **Scalability:** Easier to manage dependencies in large applications. **Example (Hilt):** """kotlin @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideUserRepository(userDataSource: UserDataSource): UserRepository { return UserRepositoryImpl(userDataSource) } @Provides @Singleton fun provideUserDataSource(apiService: ApiService): UserDataSource { return UserRemoteDataSource(apiService) } @Provides @Singleton fun provideApiService(): ApiService { // Retrofit instance creation with base URL return Retrofit.Builder() .baseUrl("https://example.com/api/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } } interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: String): User } @HiltViewModel class UserViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() { // ViewModel logic } """ ### 1.3 Single Responsibility Principle (SRP) **Standard:** Each component should have one, and only one, reason to change. * **Do This:** Break down large classes into smaller, more focused components. Ensure that each class or function performs a well-defined task. * **Don't Do This:** Create "God classes" that handle multiple unrelated responsibilities. **Why:** * **Maintainability:** Easier to understand and modify classes with a single responsibility. * **Testability:** Simpler to write unit tests for individual components. * **Reusability:** More focused components are easier to reuse in different parts of the application. **Example:** """kotlin //Before (Violates SRP) class UserProfileManager { fun loadUserProfile(userId: String) { ... } fun validateUserProfile(user: User) { ... } fun saveUserProfile(user: User) { ... } } //After (SRP Compliant): Split into multiple classes class UserProfileLoader { fun loadUserProfile(userId: String): User { ... } } class UserProfileValidator { fun validateUserProfile(user: User): Boolean { ... } } class UserProfileSaver { fun saveUserProfile(user: User) { ... } } """ ## 2. Component Design Patterns ### 2.1 Observer Pattern **Standard:** Use the Observer Pattern (through "LiveData", "Flow", or custom implementations) for asynchronous data updates. * **Do This:** Observe data changes from ViewModels in your Activities/Fragments/Composables. Prefer "StateFlow" or "SharedFlow" over "LiveData" for new development, especially for UI state management in Jetpack Compose. * **Don't Do This:** Directly modify UI elements from background threads. Poll data sources frequently, wasting resources. **Why:** * **Responsiveness:** UI updates automatically when data changes. * **Decoupling:** Observers don't need to know the details of the data source. * **Lifecycle Awareness:** "LiveData" and "Flow" are lifecycle-aware ensuring that the observer only receives updates when the component is active, preventing memory leaks. * **Concurrency Safety:** Ensures that changes that occur in background threads are properly passed to the UI. **Example ("StateFlow" in Compose):** """kotlin // ViewModel @HiltViewModel class MyViewModel @Inject constructor(): ViewModel() { private val _uiState = MutableStateFlow("Initial State") val uiState: StateFlow<String> = _uiState.asStateFlow() fun updateState(newState: String) { _uiState.value = newState } } // Composable @Composable fun MyComposable(viewModel: MyViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Text(text = uiState) Button(onClick = { viewModel.updateState("New State") }) { Text("Update State") } } """ ### 2.2 Factory Pattern **Standard:** Use the Factory Pattern to create instances of complex objects, especially when the creation logic is complex or requires dependencies. * **Do This:** Define a Factory interface or abstract class that specifies the creation method. Implement concrete factories for specific object types. * **Don't Do This:** Embed complex object creation logic directly within client classes. Use reflection unnecessarily. **Why:** * **Decoupling:** The client doesn't need to know the details of object creation * **Flexibility:** Easy to change the object creation logic without modifying the client code * **Testability:** Easier to mock the factory during testing. **Example:** """kotlin interface ViewModelFactory<T : ViewModel> { fun create(): T } class MyViewModelFactory @Inject constructor(private val repository: MyRepository) : ViewModelFactory<MyViewModel> { override fun create(): MyViewModel { return MyViewModel(repository) } } // Within an Activity or Fragment: @AndroidEntryPoint class MyActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MyViewModelFactory private val viewModel: MyViewModel by viewModels { object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return viewModelFactory.create() as T } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... rest of your code } } """ ## 3. Kotlin and Android Specific Standards ### 3.1 Kotlin Coroutines **Standard:** Leverage Kotlin Coroutines for asynchronous operations. * **Do This:** Use "viewModelScope" to manage coroutines within ViewModels which automatically cancels coroutines when the ViewModel is cleared (onCleared). Use "lifecycleScope" to simplify launching coroutines bound to Activity and Fragment lifecycles. Properly handle exceptions within coroutines using "try-catch" blocks or the "CoroutineExceptionHandler". * **Don't Do This:** Use "AsyncTask" for new development. Block the main thread with long-running operations. Neglect error handling in coroutines. **Why:** * **Concise Syntax:** Coroutines provide a cleaner and more readable way to write asynchronous code compared to callbacks or Futures. * **Structured Concurrency:** "viewModelScope" and "lifecycleScope" simplify managing the lifecycle of concurrent operations. * **Exception Handling:** The "try-catch" block provides standard exception handling for asynchronous operations. **Example:** """kotlin @HiltViewModel class MyViewModel @Inject constructor(private val repository: MyRepository) : ViewModel() { private val _data = MutableLiveData<Result<Data>>() val data: LiveData<Result<Data>> = _data fun fetchData() { _data.value = Result.Loading // Indicate loading state viewModelScope.launch { try { val result = withContext(Dispatchers.IO) { repository.getData() } // switch to IO thread _data.value = Result.Success(result) } catch (e: Exception) { _data.value = Result.Error(e) // pass along the exception } } } } """ ### 3.2 Jetpack Compose **Standard:** Utilize Jetpack Compose for building modern UIs. * **Do This:** Embrace unidirectional data flow and declarative UI programming. Use "State" and "MutableState" to manage UI state. Separate composables into small, reusable units. Employ "remember" and "rememberSaveable" to preserve state across recompositions and configuration changes. Use "CompositionLocal" to provide implicit dependencies down the Composable tree. Leverage "LaunchedEffect" and "rememberCoroutineScope" for side effects within Composables. * **Don't Do This:** Directly manipulate Views or Fragments. Perform expensive operations directly within composable functions (instead offload them to the ViewModel and use coroutines). Over-optimize recomposition without profiling. Mutate state directly without using "MutableState". **Why:** * **Declarative:** The UI is defined as a function of the state. * **Composable:** Reusable components promote code reuse and maintainability. * **Testable:** Easier to write unit tests for composable functions. * **Modern:** Takes advantages of modern best practices for UI development. **Example:** """kotlin @Composable fun MyComposable(viewModel: MyViewModel = hiltViewModel()) { val myState by viewModel.myState.collectAsStateWithLifecycle() Column { Text(text = "Value: ${myState.value}") Button(onClick = { viewModel.increment() }) { Text(text = "Increment") } } } @HiltViewModel class MyViewModel @Inject constructor() : ViewModel() { private val _myState = MutableStateFlow(Value(0)) val myState: StateFlow<Value> = _myState.asStateFlow() fun increment() { _myState.update { it.copy(value = it.value + 1) } } data class Value(val value: Int) } """ ### 3.3 Data Classes **Standard:** Use data classes for data-holding classes. * **Do This:** Use "data class" for classes mainly holding data with automatic "equals()", "hashCode()", "toString()", "copy()" generation. Ensure immutability whenever possible by using "val" for properties. * **Don't Do This:** Use regular classes for data-holding purposes, losing the benefits of automatically generated methods. Make data classes mutable unless there’s a strong reason to do so. **Why:** * **Conciseness:** Reduced boilerplate code. * **Immutability:** Promotes a more predictable and safer code. * **Equality and Hash Code:** Automatic generation of methods to properly compare objects, which is very useful in collection operations. **Example:** """kotlin data class User(val id: String, val name: String, val email: String) //Immutable data class MutableUser(var id: String, var name: String, var email: String) //Mutable (less recommended). Only use when necessary """ ### 3.4 Sealed Classes and Enums **Standard:** Use sealed classes for representing restricted class hierarchies and enums for representing a fixed set of values. * **Do This:** Use sealed classes for representing states in your application (e.g., "Loading", "Success", "Error"). Use "when" expressions with sealed classes to handle different states exhaustively. Use "enum" for limited, well-known sets of values. * **Don't Do This:** Use inheritance for situations better suited to sealed classes. Use multiple boolean flags instead of a well-defined enum. **Why:** * **Type Safety:** Sealed classes provide compile-time guarantees that all possible subtypes are handled. * **Readability:** Enhance code clarity and maintainability. * **Exhaustiveness:** The "when" expressions guarantee to check all the subclasses defined in the parent class. * **Representing State:** Can be used to describe states in a UI such as "LoadingState", "LoadedState", or "ErrorState" **Example (Sealed Class):** """kotlin sealed class Result<out T> { object Loading : Result<Nothing>() data class Success<T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } //Usage: fun handleResult(result: Result<String>) { when (result) { is Result.Loading -> showLoading() is Result.Success -> displayData(result.data) is Result.Error -> showError(result.exception) } } """ **Example (Enum):** """kotlin enum class UserRole { ADMIN, EDITOR, VIEWER } """ ### 3.5 Null Safety **Standard:** Leverage Kotlin's null safety features to avoid NullPointerExceptions. * **Do This:** Use non-null types ("String"), nullable types ("String?"), safe calls ("?."), Elvis operator ("?:"), and not-null assertions ("!!") appropriately. Favor safe calls and Elvis operator over not-null assertions. * **Don't Do This:** Use not-null assertions ("!!") without careful consideration. Ignore potential null values. **Why:** * **Prevent Crashes:** Eliminate or reduce occurrences of "NullPointerException". * **Code Reliability:** Improve code robustness and predictability. **Example:** """kotlin fun processName(user: User?) { val userName = user?.name ?: "Unknown" //Safe Call and Elvis println("User name: $userName") } """ ## 4. Performance Considerations ### 4.1 Avoid Memory Leaks **Standard:** Prevent memory leaks by properly managing object lifecycles, especially within Activities, Fragments, and Composables. * **Do This:** Unregister listeners and observers when they are no longer needed. Cancel coroutines in "onCleared" of ViewModels or "onDispose" block in Composables. Avoid holding references to Activities or Fragments in long-lived objects. Utilize "WeakReference" when necessary. * **Don't Do This:** Leak Activity or Fragment instances by holding on to them. Forget to unregister listeners, resulting in memory leaks. **Why:** * **Application Stability:** Prevents OutOfMemoryErrors and application crashes * **Improved Responsiveness:** Frees up memory, improving application performance. **Example:** """kotlin @HiltViewModel class MyViewModel @Inject constructor() : ViewModel() { private val myRepository = MyRepository() //Example Repository init { viewModelScope.launch { myRepository.startListening(::onDataChanged) //start listening } } //Cancel coroutines when the scope cancels (ViewModel disposed effectively) override fun onCleared() { super.onCleared() myRepository.stopListening() //stop callbacks from Repository when ViewModel is destroyed } private fun onDataChanged(data: String) { //data is from the repository } } class MyRepository { private var listener: ((String)-> Unit)? = null fun startListening(callback: (String) -> Unit) { listener = callback //start getting data from somewhere, and call the callback } fun stopListening() { listener = null //remove the value of the callback } } """ ### 4.2 Efficient Data Structures **Standard:** Use appropriate data structures for efficient data storage and retrieval. * **Do This:** Use "HashSet" for fast membership checks. Use "HashMap" for quick key-value lookups. Use "ArrayList" for ordered collections with efficient random access. Use specialized collections like "SparseArray" for storing sparse data. * **Don't Do This:** Use inefficient data structures, such as "LinkedList" for random access or "ArrayList" for frequent insertions/deletions in the middle of the list. **Why:** * **Performance:** Selecting the right data structure can drastically improve performance. **Example:** """kotlin //Fast membership check by using a hash set val mySet = HashSet<String>() mySet.add("apple") mySet.contains("apple") //fast check """ ### 4.3 Resource Management **Standard:** Manage resources efficiently (bitmaps, file handles, network connections) to prevent memory leaks and performance bottlenecks. * **Do This:** Use "use" block to automatically close resources. Release bitmaps when they are no longer needed. Avoid creating unnecessary objects. Cache data where appropriate. Use vector drawables instead of raster images for scalable icons. * **Don't Do This:** Leak resources by failing to close streams or recycle bitmaps. Create unnecessary objects. Download large images repeatedly without caching. **Why:** * **Performance:** Correct resource managing leads to improved responsiveness. * **Application Stability:** Prevents OutOfMemoryErrors and application crashes. **Example:** """kotlin fun readFile(file: File): String? { return try { file.inputStream().bufferedReader().use { it.readText() } //automatic close } catch (e: IOException) { null } } """ ## 5. Security Considerations ### 5.1 Data Encryption **Standard:** Encrypt sensitive data stored locally or transmitted over the network. * **Do This:** Use the Jetpack Security library for encrypting data with keys stored in the Android Keystore. Use HTTPS for network communications. * **Don't Do This:** Store sensitive data in plain text. Transmit sensitive data over unencrypted channels. Hardcode encryption keys in the code. **Why:** * **Data Confidentiality:** Protecting sensitive data from unauthorized access. * **Compliance:** Meeting regulatory requirements. ### 5.2 Input Validation **Standard:** Validate user input to prevent security vulnerabilities such as SQL injection and cross-site scripting (XSS). * **Do This:** Validate all user inputs on both the client-side and server-side. Use parameterized queries to prevent SQL injection. Encode user-supplied data before displaying it in the UI to prevent XSS.Sanitize user inputs. * **Don't Do This:** Trust user input without validation. Construct SQL queries by concatenating user input directly. Display user input directly in the UI without encoding. **Why:** * **Prevent Attacks:** Protect the application from malicious input. * **Data Integrity:** Ensures the data accuracy. ### 5.3 Permissions **Standard:** Request only the necessary permissions and handle permission requests gracefully. * **Do This:** Declare the minimum required permissions in the "AndroidManifest.xml" file. Request permissions at runtime using the Jetpack Compose's "rememberLauncherForActivityResult" or ActivityResultContracts. Ensure compliance with user data privacy regulations. Explain to the user why the permission is needed. * **Don't Do This:** Request unnecessary permissions. Request all permissions at once during the first launch. Assume that permissions are always granted. **Why:** * **User Privacy:** Respect user's privacy preferences. * **Security:** Minimize the attack surface of the application. ## Versioning This document is versioned and updated regularly to reflect the evolving best practices in Kotlin Android development. Refer to the latest version of this document for the most up-to-date guidelines.
# Performance Optimization Standards for Kotlin Android This document outlines coding standards focused specifically on performance optimization for Kotlin Android applications. Adhering to these standards helps improve application speed, responsiveness, and resource usage. It emphasizes modern approaches and patterns based on the latest Kotlin and Android features. ## I. General Principles ### I.1. Understanding Performance Bottlenecks **Goal:** Identify and address the most impactful performance issues first. * **Do This:** Utilize Android Profiler (CPU, Memory, Network, Energy) to identify hotspots. Analyze trace files generated by Systrace or Perfetto for in-depth system-level insights. * **Don't Do This:** Blindly optimize code without measuring the impact. Avoid premature optimization. * **Why:** Focusing on critical bottlenecks yields the greatest performance gains with the least effort. ### I.2. Efficient Data Structures and Algorithms **Goal:** Choose algorithms and data structures appropriate for the task. * **Do This:** Use "ArrayList" for random access, "LinkedList" for frequent insertions/deletions, "HashSet" for uniqueness checks, and "HashMap" for key-value lookups. Consider "SparseArray" or "LongSparseArray" for memory efficiency when keys are integers/longs. Use "ArrayMap" and "ArraySet" when the number of elements is small (less than a few hundred). * **Don't Do This:** Use inefficient algorithms (e.g., nested loops with O(n^2) complexity when a linear solution is possible). * **Why:** Choosing the right tool for the job reduces computational overhead. Data structure complexity dramatically affects app performance. **Example:** """kotlin // Using ArrayList for random access: val names = ArrayList<String>() names.add("Alice") names.add("Bob") names.add("Charlie") val secondName = names[1] // Efficient random access // Using HashSet for uniqueness: val uniqueNames = HashSet<String>() uniqueNames.add("Alice") uniqueNames.add("Bob") uniqueNames.add("Alice") // Duplicate - won't be added println(uniqueNames.size) // Output: 2 """ ### I.3. Avoiding Memory Leaks **Goal:** Prevent memory leaks that degrade performance and cause application crashes. * **Do This:** Unregister listeners in "onStop()" or "onDestroy()". Avoid holding long-lived references to Activities or Contexts (use "WeakReference" if necessary). Use "ViewModel" to survive configuration changes and avoid reloading data. * **Don't Do This:** Leave BroadcastReceivers registered when no longer needed. Create static references to Activity instances. * **Why:** Memory leaks accumulate over time, leading to performance degradation and eventual application crashes. **Example:** """kotlin class MyActivity : AppCompatActivity() { private var myLocationListener: LocationListener? = null private lateinit var binding: ActivityMyBinding //Example for ViewBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMyBinding.inflate(layoutInflater) setContentView(binding.root) myLocationListener = object : LocationListener { override fun onLocationChanged(location: Location) { // Handle location update } } } override fun onResume() { super.onResume() //Register Location updates Here } override fun onPause() { super.onPause() //Unregister Location Listener here } override fun onDestroy() { super.onDestroy() myLocationListener = null // Release the listener reference } } """ ### I.4. Context Usage **Goal:** Use the correct "Context" to avoid performance and memory issues. * **Do This:** Use "applicationContext" for long-lived, application-scoped operations. Use "activityContext" for UI-related operations tied to the Activity lifecycle. * **Don't Do This:** Leak activity contexts through static references or background threads. * **Why:** Using the wrong context can lead to memory leaks and unexpected behavior. ### I.5. Optimize Resources **Goal:** Reducing the size and quality of images and other assets. * **Do This:** * Use WebP format for images (smaller size, better compression). * Use vector drawables for simple icons to avoid scaling artifacts and reduce APK size. * Minimize the size of audio and video files. * Use tools like "R8" to remove unused resources. * **Don't Do This:** Include large, unoptimized images directly in the APK. Use high-resolution images when lower resolution is sufficient. * **Why:** Smaller APKs download faster and consume less storage space. Optimized resources reduce memory usage and improve rendering performance. ## II. Kotlin-Specific Optimizations ### II.1. Inline Functions **Goal:** Reduce function call overhead for small, frequently called functions. * **Do This:** Use "inline" functions for lambdas and small functions that operate directly on their arguments. Analyze the bytecode impact. * **Don't Do This:** Inline large functions, as it can increase bytecode size. Inline functions with complex control flow. * **Why:** Inlining replaces the function call with the function's code directly, eliminating the call overhead. **Example:** """kotlin inline fun measureTimeMillis(block: () -> Unit): Long { val start = System.currentTimeMillis() block() return System.currentTimeMillis() - start } fun main() { val time = measureTimeMillis { // Some code to measure Thread.sleep(100) } println("Time taken: $time ms") } """ ### II.2. "Sequence" for Lazy Evaluation **Goal:** Optimize collection processing for very large datasets. * **Do This:** Use "Sequence" for chains of operations on large collections when intermediate results don't need to be stored. * **Don't Do This:** Use "Sequence" for small collections, as the overhead of creating and managing the sequence may outweigh the benefits. * **Why:** Sequences perform operations lazily, processing elements only when needed, which can save memory and time. **Example:** """kotlin val numbers = (1..1000000).asSequence() .filter { it % 2 == 0 } .map { it * 2 } .take(10) .toList() println(numbers) """ ### II.3. Avoiding Boxing/Unboxing **Goal:** Minimize the overhead of converting between primitive types and their object wrappers. * **Do This:** Use primitive types ("Int", "Long", "Float", "Boolean") whenever possible. Avoid nullable primitive types (e.g., "Int?") unless nullability is required. Use specialized collections for primitive types (e.g., "IntArray", "LongArray"). * **Don't Do This:** Use "Integer" and other wrapper classes unnecessarily. * **Why:** Boxing and unboxing operations are computationally expensive. **Example:** """kotlin //Using primitive types: val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5) //Avoid: val boxedNumbers: Array<Int> = arrayOf(1, 2, 3, 4, 5) // Avoid """ ### II.4. Coroutines for Asynchronous Operations **Goal:** Handle long-running operations without blocking the main thread. * **Do This:** Use "coroutines" for network requests, database operations, and other I/O-bound tasks. Use "withContext(Dispatchers.IO)" to offload work to a background thread pool. Use "Dispatchers.Default" for CPU-intensive tasks. * **Don't Do This:** Perform long-running operations directly on the main thread. Use "Thread" directly (coroutines are more lightweight and easier to manage). * **Why:** Blocking the main thread leads to ANRs (Application Not Responding) and a poor user experience. **Example:** """kotlin import kotlinx.coroutines.* fun main() = runBlocking { val result = withContext(Dispatchers.IO) { // Simulate a network request delay(2000) "Data from network" } println(result) // Output: Data from network (after 2 seconds) } """ ### II.5. Delegates for Property Optimization **Goal:** Use "lazy" and "observable" delegates to optimize property initialization and observation. * **Do This:** Use "lazy" for expensive property initialization that should only occur when the property is first accessed. Use "observable" or "vetoable" when reacting property changes is required * **Don't Do This:** Perform expensive initialization in the constructor unnecessarily. * **Why:** Lazy initialization defers computation until it is actually needed, saving resources. **Example:** """kotlin val expensiveProperty: String by lazy { // Perform an expensive operation here (e.g., reading from a file) println("Initializing expensiveProperty") "Result of expensive operation" } fun main() { println("Before accessing expensiveProperty") println(expensiveProperty) // Initialization happens here println(expensiveProperty) // Value is cached } """ ### II.6. Data Classes and Immutability **Goal:** Utilizing Data Classes for Immutable Data Structures. * **Do This:** Use "data class" where appropriate. Use immutable collections (e.g., "listOf", "mapOf", "setOf"). Leverage Kotlin's copy() function for data class to create new instances with selective changes. * **Don't Do This:** Use mutable data structures when immutability is sufficient. Modify data objects directly. * **Why:** Immutability simplifies concurrent programming, reduces bugs, and can improve performance by allowing for caching and optimized comparison operations. **Example:** """kotlin data class User(val id: Int, val name: String, val email: String) fun main() { val user1 = User(1, "Alice", "alice@example.com") val user2 = user1.copy(name = "Alicia") // Create a new instance with a modified name println(user1) println(user2) } """ ## III. Android-Specific Optimizations ### III.1. RecyclerView Optimization **Goal:** Optimize the performance of "RecyclerView" for smooth scrolling. * **Do This:** Use "DiffUtil" to calculate the minimal set of changes when updating the RecyclerView's data. Use "RecyclerView.ViewHolder" pattern to cache view lookups. Avoid complex layouts in RecyclerView items. Set "setHasFixedSize(true)" if the RecyclerView's size is fixed. * **Don't Do This:** Call "notifyDataSetChanged()" unnecessarily. Perform expensive operations within the "onBindViewHolder()" method. * **Why:** RecyclerView is a core component for displaying lists of data. Efficient RecyclerView usage is critical for a smooth user experience. **Example:** """kotlin class MyAdapter(private var items: List<MyItem>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val textView: TextView = itemView.findViewById(R.id.textView) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_my_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = items[position] holder.textView.text = item.text } override fun getItemCount(): Int { return items.size } fun updateItems(newItems: List<MyItem>) { val diffResult = DiffUtil.calculateDiff(MyDiffCallback(this.items, newItems)) this.items = newItems diffResult.dispatchUpdatesTo(this) } class MyDiffCallback(private val oldList: List<MyItem>, private val newList: List<MyItem>) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } } } """ ### III.2. View Inflation Optimization **Goal:** Reduce the time it takes to inflate layouts. * **Do This:** Use "ViewBinding" to avoid "findViewById" calls. Use "ConstraintLayout" effectively to reduce layout nesting. Avoid overdraw. Use "<include>" and "<merge>" tags to reuse layouts. * **Don't Do This:** Inflate complex layouts frequently. Overuse nested layouts. * **Why:** View inflation is a common performance bottleneck. **Example:** """xml <!-- Example of merging layout --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <include layout="@layout/reusable_layout"/> </LinearLayout> """ ### III.3. Network Optimization **Goal:** Reduce network latency and data usage. * **Do This:** Use GZIP compression for network requests. Cache network responses. Use pagination or incremental loading for large datasets. Optimize image sizes and formats. Use "OkHttp" for efficient HTTP client management. Use "Retrofit" for type-safe REST API access. Use "DataStore" for persisting key-value pairs and typed objects asynchronously and transactionally. * **Don't Do This:** Download large amounts of data unnecessarily. Make frequent, small network requests. * **Why:** Network operations are inherently slow. **Example:** """kotlin // Using Retrofit for network requests: interface ApiService { @GET("/users") suspend fun getUsers(): List<User> } val retrofit = Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build() val apiService = retrofit.create(ApiService::class.java) fun main() = runBlocking { val users = apiService.getUsers() println(users) } """ ### III.4. Database Optimization **Goal:** Improve database query performance and reduce data storage footprint. * **Do This:** Use indices on frequently queried columns. Use transactions to group multiple database operations. Use "SQLite" efficiently (e.g., prepared statements). Use "Room" persistence library for type-safe database access. Apply appropriate data validation before saving to the db. Use pagination for large datasets * **Don't Do This:** Perform complex queries on the main thread. Fetch all columns when only a few are needed. * **Why:** Database operations can be slow. **Example:** """kotlin //Using Room Persistence Library: @Entity data class User( @PrimaryKey val id: Int, val name: String, @ColumnInfo(name = "email_address") val email: String ) @Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> @Insert fun insertAll(vararg users: User) } @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } """ ### III.5. Battery Optimization **Goal:** Minimize battery consumption. * **Do This:** Use JobScheduler or WorkManager for deferrable background tasks. Minimize wakelocks. Batch network requests. Optimize location updates. Avoid unnecessary CPU usage. * **Don't Do This:** Continuously poll for updates. Keep the screen on unnecessarily. * **Why:** Excessive battery drain leads to a poor user experience. ### III.6. Profiling and Monitoring **Goal:** Continuously monitor the application performance and identify new bottlenecks. * **Do This:** Use Android Profiler during development. Integrate crash reporting tools (e.g., Firebase Crashlytics). Monitor app performance in production using Firebase Performance Monitoring. Use tools like LeakCanary to detect memory leaks in debug builds. * **Don't Do This:** Ignore performance issues reported by users. * **Why:** Continuous monitoring allows for proactive identification and resolution of performance issues. These standards provide a solid foundation for building high-performance Kotlin Android applications. Consistent application of these principles will significantly improve the user experience and the overall quality of the software. Regularly review and update these standards to reflect the latest best practices and advancements in the Kotlin and Android platforms.
# Testing Methodologies Standards for Kotlin Android This document specifies the coding standards and best practices for testing methodologies in Kotlin Android development. Adhering to these guidelines ensures robust, maintainable, and high-quality applications. ## 1. General Testing Principles * **Standard:** Prioritize testability from the start. Design code with testing in mind, making it easier to isolate components and verify behavior. * **Why:** Testable code is easier to understand, debug, and maintain. It also reduces the risk of introducing bugs during refactoring. * **Do This:** Embrace dependency injection, use interfaces for abstraction, and avoid tightly coupled designs. * **Don't Do This:** Avoid creating monolithic classes with intertwined responsibilities, making them difficult to test in isolation. Don't rely heavily on static methods or global state. * **Standard:** Aim for a comprehensive testing suite covering unit, integration, and end-to-end tests. * **Why:** Different types of tests catch different types of bugs. A well-rounded testing strategy ensures a higher level of confidence in the application's correctness. * **Do This:** Implement a testing pyramid approach, with a large number of unit tests, a moderate number of integration tests, and a smaller number of end-to-end tests. * **Don't Do This:** Neglect any of the test levels. Over-relying on one type of test can leave gaps in coverage and increase the risk of overlooking bugs. * **Standard:** Write tests alongside production code. * **Why:** Writing tests concurrently helps to define clear requirements upfront and reduces the temptation to skip testing due to time constraints. * **Do This:** Adopt Test-Driven Development (TDD) or Behavior-Driven Development (BDD) workflows, where tests are written before the corresponding production code. * **Don't Do This:** Postpone testing until the end of a development cycle. ## 2. Unit Testing ### 2.1. Scope * **Standard:** Unit tests should focus on verifying the behavior of individual classes or functions in isolation. * **Why:** Unit tests provide fast feedback on the correctness of small code units and help to identify bugs early in the development process. * **Do This:** Mock or stub dependencies to isolate the code under test. Ensure each test covers a single, well-defined aspect of the unit's behavior. * **Don't Do This:** Test multiple units of code within a single unit test or rely on external resources (e.g., databases, network connections) in unit tests. Avoid logic within tests. ### 2.2. Libraries and Frameworks * **Standard:** Use well-established testing libraries such as JUnit, Mockito, and Turbine. * **Why:** These libraries provide the necessary tools for writing and running unit tests, creating mocks, and asserting expected results. * **Do This:** Leverage JUnit for test structure and execution. Utilize Mockito or Mockk to create mock objects for dependencies. Use Turbine for testing Flow-based code. * **Don't Do This:** Re-invent the wheel by creating custom mocking frameworks or relying on deprecated testing libraries. * **Standard:** Utilize Coroutines Test APIs for testing asynchronous code. * **Why:** Ensuring proper testability of components using Coroutines is crucial for stability. * **Do This:** Inject "TestDispatcher" instances into the class under test. Use "runTest" to execute tests on a controlled dispatcher. Use "advanceUntilIdle()" to execute all pending coroutines. * **Don't Do This:** Block the main thread with "Thread.sleep" or similar methods while testing. Avoid having components directly instantiating dispatcher objects. Dispatchers should always be resolved by dependency injection. ### 2.3. Example """kotlin import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher class MyViewModelTest { private val repository: MyRepository = mock() @OptIn(ExperimentalCoroutinesApi::class) @Test fun "getData updates state with result when successful"() = runTest { //Given val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(testScheduler) val viewModel = MyViewModel(repository, testDispatcher) val expectedData = "Test Data" whenever(repository.fetchData()).thenReturn(Result.success(expectedData)) //When viewModel.fetchData() //This calls viewModel.uiState.update { it.copy(data = result.getOrNull() } advanceUntilIdle() // this line is crucial when using Turbine for testing Flows // OR manually collect state depending on need. // val actualData = viewModel.uiState.first().data // assertEquals(expectedData, actualData) //Then with Turbine Turbine.test(viewModel.uiState) { val state = awaitItem() // initial state assertEquals(null, state.data) //verify initial state val newState = awaitItem() assertEquals(expectedData, newState.data) // verify state after call finished cancelAndIgnoreRemainingEvents() } } //Add more test cases for error scenarios, loading states, etc. } class MyRepository { suspend fun fetchData(): Result<String> { TODO("Not yet implemented") } } """ ### 2.4. Naming Conventions * **Standard:** Give descriptive names to test classes and test methods to clearly communicate their purpose. * **Why:** Clear names make it easier to understand what each test is verifying and to quickly identify failing tests. * **Do This:** Follow a convention such as "[ClassUnderTest]_[Scenario]_[ExpectedResult]". * **Don't Do This:** Use generic names like "testMethod1" or "MyClassTest" without indicating what the test is intended to verify. ## 3. Integration Testing ### 3.1. Scope * **Standard:** Integration tests should verify the interaction between multiple components or modules within the application. * **Why:** Integration tests catch bugs that arise from the interplay between different parts of the system, ensuring they work together correctly. * **Do This:** Focus on testing the communication paths and data flow between components. Use real dependencies or lightweight test doubles (e.g., in-memory databases). * **Don't Do This:** Overlap with unit tests by testing individual components in isolation or replicate end-to-end tests by testing the entire application flow. ### 3.2. Android Specific Integration Tests * **Standard:** Use Android's testing support library for instrumented tests. * **Why:** Provides APIs to interact with Android components like Activities, Fragments and Services through the *Context*. * **Do This:** Utilise "AndroidJUnit4" test runner. Use "ActivityScenario" to launch and control activities during the test. Use "Espresso" to interact with the UI. * **Don't Do This:** Running integration tests as unit tests without the required Android runtime environment. ### 3.3 Example """Kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityScenarioRule import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withText @RunWith(AndroidJUnit4::class) class MyActivityIntegrationTest { @Rule @JvmField var activityScenarioRule = ActivityScenarioRule(MyActivity::class.java) @Test fun testButtonClickChangesText() { // Arrange val buttonId = R.id.myButton val textViewId = R.id.myTextView val expectedText = "Button Clicked!" // Act onView(withId(buttonId)).perform(click()) // Assert onView(withId(textViewId)).check(matches(withText(expectedText))) } } """ ## 4. End-to-End (E2E) Testing ### 4.1. Scope * **Standard:** End-to-end tests should verify the entire application flow from start to finish, simulating real user interactions. * **Why:** E2E tests ensure that all parts of the system work together correctly and that the application meets the overall user requirements. * **Do This:** Use UI testing frameworks (e.g., Espresso, UI Automator) to interact with the application's user interface. Test against a production-like environment or a staging environment. * **Don't Do This:** Over-rely on E2E tests to catch low-level bugs that should be caught by unit or integration tests. Avoid testing implementation details through the UI. Don't put too many assertions. ### 4.2. Libraries and Frameworks * **Standard:** Consider using UI Automator or Espresso for UI testing. * **Why:** These frameworks provide APIs to interact with UI elements, perform actions, and assert expected results. * **Do This:** Use Espresso for testing within your own application. Use UI Automator for testing across application boundaries or system features. Consider libraries like Kakao or Compose UI testing to simplify UI testing. * **Don't Do This:** Attempt to write UI tests without using a dedicated UI testing framework or rely on manual testing as the primary means of verifying the UI. ### 4.3. Example """kotlin import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LoginE2ETest { @Rule @JvmField var activityScenarioRule = ActivityScenarioRule(LoginActivity::class.java) @Test fun testLoginSuccess() { // Arrange - Assume valid credentials val username = "valid_user" val password = "valid_password" // Act onView(withId(R.id.usernameEditText)).perform(typeText(username)) onView(withId(R.id.passwordEditText)).perform(typeText(password)) onView(withId(R.id.loginButton)).perform(click()) // Assert - Verify successful login (e.g., navigate to the main activity) onView(withId(R.id.welcomeTextView)).check(matches(withText("Welcome, $username!"))) } // Add more test cases for invalid credentials, error scenarios, etc. } """ ### 4.4. Testing in Jetpack Compose * **Standard:** Use "ComposeTestRule" for testing composables. * **Why:** "ComposeTestRule" offers tools to find, interact with, and assert state within the Compose UI tree. * **Do This:** Utilize "setContent { }" to set the content of the screen. Use "onNodeWithTag()" or "onNodeWithContentDescription" to find elements. Use "performClick()" or "performTextInput()" to simulate user actions. "assertIsDisplayed()" or "assertTextEquals()" to assert the state of composition. * **Don't Do This:** Directly access or modify internal state of your composables within tests. Focus on observing the state transitions as a result of user interactions. ### 4.5. Example """kotlin import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import org.junit.Rule import org.junit.Test class MyComposeScreenTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testInputAndDisplay() { // Arrange val inputString = "Hello, Compose!" composeTestRule.setContent { MyComposeScreen() // Replace with your actual Composable function } // Act composeTestRule.onNodeWithTag("inputField").performTextInput(inputString) composeTestRule.onNodeWithTag("submitButton").performClick() // Assert composeTestRule.onNodeWithTag("outputText").assertIsDisplayed().assertTextEquals(inputString) } } """ ## 5. Test Doubles * **Standard:** Use test doubles (mocks, stubs, fakes) to isolate the code under test and control its dependencies. * **Why:** Test doubles allow you to simulate the behavior of dependencies without relying on their actual implementation, making tests faster, more predictable, and more reliable. * **Do This:** Use mocks to verify interactions with dependencies. Use stubs to provide predefined responses from dependencies. Use fakes to provide simplified implementations of dependencies. * **Don't Do This:** Overuse mocks, mock everything or create complex mock setups that mirror the actual implementation. Do not put logic inside mocks. Favour stubs over mocks when you don't care about verifying interactions. ## 6. Code Coverage * **Standard:** Use code coverage tools to measure the percentage of code covered by tests. * **Why:** Code coverage provides a metric to assess the completeness of the testing suite and identify areas that need more testing. * **Do This:** Use tools like JaCoCo to generate code coverage reports. Aim for a reasonable level of coverage (e.g., 80-90%), but don't treat coverage as the sole indicator of test quality. Focus on covering critical paths and high-risk areas. Use the Coverage report, not as an end-goal, but rather as a 'hint' to what areas might benefit from more testing. * **Don't Do This:** Strive for 100% coverage at the expense of test quality. Don't ignore low coverage areas without investigating the reasons. ## 7. Test Execution and Reporting * **Standard:** Automate test execution through continuous integration (CI) pipelines. * **Why:** Automated test execution ensures that tests are run regularly and that any regressions are detected early. * **Do This:** Integrate test execution into your CI/CD workflow. Generate test reports and make them easily accessible to the team. * **Don't Do This:** Rely on manual test execution or neglect to monitor test results in the CI environment. * **Standard:** Utilize Gradle managed devices for consistent test execution. * **Why:** Using emulators managed by Gradle ensures that all developers and CI systems use the same test environment. * **Do This:** Configure emulators with the Android Gradle Plugin and execute tests with the specified device. * **Don't Do This:** Rely on locally installed devices/emulators, which may have different configuration. ## 8. Test Data Management * **Standard:** Manage test data effectively to ensure tests are repeatable and reliable. * **Why:** Consistent test data is important to minimize test flakiness and ensures that tests always start from a known state. * **Do This:** Use a dedicated test database or test fixtures to provide test data. Avoid modifying shared data in tests and reset the database or fixtures after each test. * **Don't Do This:** Hard-coding data in your tests. Avoid data dependencies between tests. * **Standard**: Favor using Kotlin data classes over complex objects for test data. * **Why**: Data classes provide default "equals()", "hashCode()", and "toString()" implementations. * **Do This**: Use Kotlin data classes. * **Don't Do This**: Utilize complex class structures unless required. ## 9. Addressing Flaky Tests * **Standard:** Strive to eliminate flaky tests * **Why:** Flaky tests undermine confidence in the testing suite, and hide real bugs. * **Do This:** Understand that root cause of flakiness. Add retries with exponential backoff for transient issues. Increase timeouts within reason. Disable extremely flaky tests. * **Don't Do This:** Ignore flaky tests. Mark flaky tests as a 'known failure' and move on. ## 10. Monitoring * **Standard:** Always collect metrics for crash-free users, ANRs, and slow rendering. * **Why:** Crash-free user metrics directly represent the reliability of the system from a user perspective. ANRs and slow rendering are also important UX metrics. * **Do This:** Use well-established libraries like Firebase Crashlytics. Setup alerts to be notified when metrics fall below certain thresholds. * **Don't Do This:** Focus only on collecting exceptions. Ignore monitoring entirely because it's 'someone else's job'. ## 11. Test Pyramid * **Standard:** Unit tests should be the base of the pyramid, followed by integration tests, and finally, UI/E2E tests at the top. * **Why:** Unit tests are fast and cheap while UI tests are slow and expensive. Maintaining a balance between them is crucial to prevent regressions. * **Do This:** Write a lot of unit tests, fewer integration tests and even fewer UI tests. * **Don't Do This:** Rely heavily on UI tests and neglect unit tests. These standards, when followed, yield a good balance between code that's concise, easier to understand, debug and maintain as well as ensuring fewer regressions, fast debug cycles and high quality applications.
# API Integration Standards for Kotlin Android This document outlines the coding standards and best practices for API integration in Kotlin Android applications. It aims to provide guidance for developers to build robust, maintainable, and performant Android applications that interact with backend services and external APIs. These standards are designed to be consistent with modern Kotlin and Android development approaches. ## 1. Architecture and Design ### 1.1. Layered Architecture **Standard:** Implement a layered architecture to separate concerns and improve maintainability. This typically includes: * **Data Layer:** Handles data retrieval and persistence (API calls, database interactions, etc.). * **Domain Layer:** Contains business logic and use cases. * **Presentation Layer (UI Layer):** Handles user interface and interaction logic (Activities, Fragments, Composable functions, ViewModels, etc.). * **Dependency Injection Layer (Optional but recommended):** Manages dependencies between layers. Use Hilt (recommended) or Koin. **Why:** Layered architecture promotes separation of concerns, making the codebase more modular and easier to test, maintain, and scale. **Do This:** """kotlin // Data Layer (Repository) class UserRepository(private val apiService: ApiService) { suspend fun getUser(userId: String): Result<User> { return try { val response = apiService.getUser(userId) if (response.isSuccessful) { Result.Success(response.body()!!) } else { Result.Error(Exception("API Error: ${response.code()}")) } } catch (e: Exception) { Result.Error(e) } } } // Domain Layer (UseCase) class GetUserUseCase(private val userRepository: UserRepository) { suspend operator fun invoke(userId: String): Result<User> { return userRepository.getUser(userId) } } // Presentation Layer (ViewModel) class UserViewModel( private val getUserUseCase: GetUserUseCase, savedStateHandle: SavedStateHandle // Use SavedStateHandle for persisting state across config changes ) : ViewModel() { private val _user = MutableStateFlow<Result<User>>(Result.Loading) val user: StateFlow<Result<User>> = _user.asStateFlow() val userId: String = savedStateHandle.get<String>("userId") ?: throw IllegalArgumentException("Missing user ID") init { loadUser() } private fun loadUser() { viewModelScope.launch { _user.value = getUserUseCase(userId) } } } """ **Don't Do This:** """kotlin // Anti-pattern: Mixing UI and API calls in the Activity/Fragment/Composable function class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Network call directly in the Activity! BAD! CoroutineScope(Dispatchers.Main).launch { val apiService = Retrofit.Builder() .baseUrl("https://example.com/api/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) try { val response = apiService.getUser("123") if (response.isSuccessful) { // Update UI } else { // Handle error } } catch (e: Exception) { // Handle exception } } } } """ ### 1.2. Data Transfer Objects (DTOs) **Standard:** Use DTOs to represent the data received from APIs. Map these DTOs to domain models within the data layer. **Why:** DTOs decouple your application's internal data models from the API's data structure. This allows you to adapt to API changes without affecting the entire application and improves testability. **Do This:** """kotlin // API Response DTO data class UserDto( val id: String, val username: String, val email: String ) // Domain Model data class User( val id: String, val username: String, val email: String ) // Data Layer (Repository) class UserRepository(private val apiService: ApiService) { suspend fun getUser(userId: String): Result<User> { return try { val response = apiService.getUser(userId) if (response.isSuccessful) { val userDto = response.body()!! val user = User(userDto.id, userDto.username, userDto.email) // Mapping DTO to Domain Model Result.Success(user) } else { Result.Error(Exception("API Error: ${response.code()}")) } } catch (e: Exception) { Result.Error(e) } } } """ **Don't Do This:** """kotlin // Anti-pattern: Using the API response class directly in the UI layer data class UserDto( //This becomes part of the UI layer's direct dependency val id: String, val username: String, val email: String ) // ViewModel class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _user = MutableLiveData<UserDto>() // Exposing DTO directly to the UI! BAD! val user: LiveData<UserDto> = _user fun loadUser(userId: String) { viewModelScope.launch { val result = userRepository.getUser(userId) if (result is Result.Success) { _user.value = result.data // Directly assigning API response to UI } else { // Handle error } } } } """ ### 1.3. Handling API Keys and Secrets **Standard:** Never hardcode API keys or secrets directly in your code. Use secure methods to store and access them, such as: * **Build Config Fields:** Use "buildConfigField" in your "build.gradle.kts" file to store API keys. These are compiled into the app and are more difficult to extract than resources. * **Secrets Gradle Plugin:** Use the "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" to securely manage secrets from a "secrets.properties" file during build time. * **Backend-for-Frontend (BFF):** Consider using a BFF to proxy API calls and manage secrets on the server side. This reduces exposure of secrets to the mobile client. **Why:** Hardcoding API keys compromises the security of your application and can lead to unauthorized access to your API accounts. **Do This:** """kotlin // build.gradle.kts (Module: app level) plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") //Secrets Gradle Plugin } android { buildTypes { release { buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") // Build config field // ... } debug { buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") // Build config field // ... } } //... } """ """kotlin //Accessing API Key (Kotlin) val apiKey = BuildConfig.API_KEY """ **secrets.properties:** """properties MAPS_API_KEY=YOUR_ACTUAL_MAPS_API_KEY """ **Don't Do This:** """kotlin // Anti-pattern: Hardcoding API Key val apiKey = "YOUR_API_KEY" // Never do this! """ ## 2. Retrofit and Networking ### 2.1. Retrofit Client **Standard:** Use Retrofit for making network requests. Configure the Retrofit client with appropriate settings, including: * **Base URL:** Define a constant for the base URL. * **Converter Factory:** Use "GsonConverterFactory" or "KotlinSerializationConverterFactory" (for Kotlinx Serialization). * **OkHttpClient:** Configure an "OkHttpClient" with timeouts, interceptors (for logging and authentication), and caching. * **Error Handling:** Use "try-catch" blocks or Kotlin's "Result" type to handle network errors gracefully. **Why:** Retrofit simplifies network requests, handles serialization/deserialization, and provides a clean, type-safe API. **Do This:** """kotlin import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object ApiClient { private const val BASE_URL = "https://api.example.com/" private val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // Log request and response details } private val okHttpClient = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .connectTimeout(30, TimeUnit.SECONDS) //Set timeouts .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() val retrofit: Retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) //Use Gson or Kotlinx Serialization .client(okHttpClient) .build() } // API Interface interface ApiService { @GET("users/{userId}") suspend fun getUser(@Path("userId") userId: String): retrofit2.Response<UserDto> } // Usage val apiService = ApiClient.retrofit.create(ApiService::class.java) """ **Don't Do This:** """kotlin // Anti-pattern: Creating a new Retrofit instance for every request fun getUser(userId: String): UserDto? { val retrofit = Retrofit.Builder() // Creating a new Retrofit instance every time! BAD! .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() val apiService = retrofit.create(ApiService::class.java) //... } """ ### 2.2. Coroutines and Flow **Standard:** Use Kotlin Coroutines and Flow for asynchronous network requests. Retrofit supports suspending functions, which can be called directly from coroutines. Use "flow" builder to emit data from the network layer to the UI. **Why:** Coroutines provide a concise and efficient way to handle asynchronous operations, making your code more readable and maintainable. Flow provides a reactive stream of data. **Do This:** """kotlin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.Dispatchers interface ApiService { @GET("users/{userId}") suspend fun getUser(@Path("userId") userId: String): Response<UserDto> } class UserRepository(private val apiService: ApiService) { fun getUserFlow(userId: String): Flow<Result<User>> = flow { emit(Result.Loading) // Emit Loading state try { val response = apiService.getUser(userId) if (response.isSuccessful) { val userDto = response.body()!! val user = User(userDto.id, userDto.username, userDto.email) emit(Result.Success(user)) // Emit Success with data } else { emit(Result.Error(Exception("API Error: ${response.code()}"))) //Emit Error } } catch (e: Exception) { emit(Result.Error(e)) //Emit Exception } }.flowOn(Dispatchers.IO) // Run on I/O thread } // ViewModel to collect the Flow class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _userState = MutableStateFlow<Result<User>>(Result.Loading) val userState: StateFlow<Result<User>> = _userState.asStateFlow() fun loadUser(userId: String) { viewModelScope.launch { userRepository.getUserFlow(userId) .collect { result -> _userState.value = result } } } } """ **Don't Do This:** """kotlin // Anti-pattern: Using callbacks for API calls fun getUser(userId: String, callback: (UserDto?) -> Unit) { // Avoid callbacks! val apiService = ApiClient.retrofit.create(ApiService::class.java) val call = apiService.getUser(userId) call.enqueue(object : Callback<UserDto> { override fun onResponse(call: Call<UserDto>, response: Response<UserDto>) { callback(response.body()) } override fun onFailure(call: Call<UserDto>, t: Throwable) { callback(null) } }) } """ ### 2.3. Error Handling with "Result" **Standard:** Use Kotlin's "Result" type (or a custom "Result" sealed class) to represent the outcome of API calls. This allows you to handle success, error, and loading states in a type-safe manner. **Why:** "Result" type forces you to handle both success and error scenarios preventing crashes and providing a better user experience. **Do This:** """kotlin // Define a Result sealed class if you're not targeting Android 13+ (which includes Result) sealed class Result<out T> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() object Loading : Result<Nothing>() } //Api Service & Repository (as shown in previous examples using Result type throughout) """ **Don't Do This:** """kotlin // Anti-pattern: Returning null on error fun getUser(userId: String): UserDto? { // Returning null on error!BAD! try { val response = apiService.getUser(userId) return response.body() } catch (e: Exception) { return null // Returning null } } """ ### 2.4. Pagination and List Handling **Standard:** Implement pagination when dealing with large datasets from APIs. Use appropriate query parameters to request data in chunks. Display a loading indicator while fetching more data. **Why:** Pagination improves performance and prevents your app from crashing due to excessive memory usage. It also provides a better user experience. **Do This:** """kotlin // API Interface interface ApiService { @GET("users") suspend fun getUsers( @Query("page") page: Int, @Query("limit") limit: Int ): Response<List<UserDto>> } // Repository class UserRepository(private val apiService: ApiService) { suspend fun getUsers(page: Int, limit: Int): Result<List<User>> { return try { val response = apiService.getUsers(page, limit) if (response.isSuccessful) { //... map to User objects } //... } //... } } // ViewModel (using Paging 3 library) class UserViewModel(private val userRepository: UserRepository) : ViewModel() { val userFlow: Flow<PagingData<User>> = Pager( config = PagingConfig(pageSize = 20, enablePlaceholders = false), pagingSourceFactory = { UserPagingSource(userRepository) } ).flow.cachedIn(viewModelScope) } // PagingSource class UserPagingSource( private val userRepository: UserRepository ) : PagingSource<Int, User>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> { val pageNumber = params.key ?: 1 val pageSize = params.loadSize return when (val result = userRepository.getUsers(pageNumber, pageSize)) { is Result.Success -> { val users = result.data LoadResult.Page( data = users, prevKey = if (pageNumber == 1) null else pageNumber - 1, nextKey = if (users.isEmpty()) null else pageNumber + 1 ) } is Result.Error -> { LoadResult.Error(result.exception) } Result.Loading -> { LoadResult.Error(Exception("Unexpected loading state")) // Handle loading within paging source is odd but possible } } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } } //In the UI use collectAsState(context = Dispatchers.IO.asExecutor()) to display paginated data """ **Don't Do This:** """kotlin // Anti-pattern: Fetching all data at once suspend fun getAllUsers(): List<UserDto> { // Fetching all users at once! BAD! val response = apiService.getAllUsers() return response.body() ?: emptyList() } """ ## 3. Data Caching and Offline Support ### 3.1. Caching Strategies **Standard:** Implement caching strategies to improve performance and provide offline support. Consider using: * **In-memory caching:** Cache frequently accessed data in memory using "LruCache" or similar mechanisms. * **Disk-based caching:** Use Room database to cache data persistently on disk. * **Network caching:** Use OkHttp's built-in caching mechanisms by configuring "Cache-Control" headers. **Why:** Caching reduces network requests, improves app responsiveness, and allows users to access data even when offline. **Do This:** """kotlin // OkHttp Cache Configuration val cacheSize = 10 * 1024 * 1024L // 10MB val cache = Cache(context.cacheDir, cacheSize) """ """kotlin // Add Interceptor for cache control val okHttpClient = OkHttpClient.Builder() .cache(cache) .addInterceptor { chain -> var request = chain.request() request = if (isNetworkAvailable(context)) request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build() // Cache for 1 minute when online else request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build() // Cache for 7 days when offline chain.proceed(request) } .build() // Function to check network availability (implement this based on ConnectivityManager) fun isNetworkAvailable(context: Context): Boolean { // Implementation... return false; } """ **Don't Do This:** """kotlin // Anti-pattern: Not implementing any caching suspend fun getUser(userId: String): UserDto? { // No caching! val response = apiService.getUser(userId) return response.body() } """ ### 3.2. Offline Data Synchronization **Standard:** Implement a mechanism to synchronize offline data with the backend when the device comes back online. Use a background service or WorkManager to handle synchronization. **Why:** Offline synchronization ensures data consistency and provides a seamless user experience even with intermittent network connectivity. **Do This:** """kotlin // WorkManager for syncing data in the background class SyncWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { private val userRepository = UserRepository(/*...*/); override suspend fun doWork(): Result { return try { val unsyncedData = // get unsynced data userRepository.syncData(unsyncedData) // Call API to sync Result.success() } catch (e: Exception) { Result.retry() // Retry the work later } finally { //Cleanup work } } } // Schedule the worker val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS) // Sync every hour .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "syncData", ExistingPeriodicWorkPolicy.KEEP, // Or REPLACE syncRequest ) """ ## 4. Security Considerations ### 4.1. HTTPS **Standard:** Always use HTTPS for all API communication to encrypt data in transit. **Why:** HTTPS protects sensitive data from eavesdropping and tampering. **Do This:** """kotlin // Correct: Using HTTPS private const val BASE_URL = "https://api.example.com/" // Use HTTPS """ **Don't Do This:** """kotlin // Incorrect: Using HTTP private const val BASE_URL = "http://api.example.com/" // Never use HTTP """ ### 4.2. Input Validation **Standard:** Validate all user input and API responses to prevent injection attacks and data corruption. **Why:** Input validation prevents malicious data from being processed by your application or the backend. **Do This:** """kotlin fun validateUserId(userId: String): Boolean { // Check if userId is valid (e.g., alphanumeric, length) return userId.matches(Regex("[a-zA-Z0-9]+")) && userId.length in 5..20 } """ ### 4.3. Data Encryption **Standard:** Encrypt sensitive data stored locally using appropriate encryption algorithms. **Why:** Data encryption protects data even if the device is compromised. ## 5. Testing ### 5.1. Unit Testing **Standard:** Write unit tests for your data layer (repositories, use cases) to verify that API calls are made correctly, data is parsed correctly, and errors are handled properly. Mock the "ApiService" using Mockito or Mockk. **Why:** Unit tests ensure the reliability of your API integration logic. **Do This:** """kotlin import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test class UserRepositoryTest { @Test fun "getUser should return success when API call is successful"() = runBlocking { // Arrange val mockApiService = mockk<ApiService>() // Create a Mock ApiService coEvery { mockApiService.getUser("123") } returns Response.success(UserDto("123", "test", "test@example.com")) // mock the result val userRepository = UserRepository(mockApiService) // Act val result = userRepository.getUser("123") // Assert assert(result is Result.Success) assertEquals("123", (result as Result.Success).data.id) } } """ ## 6. Monitoring and Logging ### 6.1. Logging **Standard:** Use a logging framework (Timber) to log API requests, responses, and errors. Log relevant information such as request URLs, response codes, and error messages. However, be very careful not to log sensitive data such as passwords or API keys. **Why:** Logging helps you monitor API usage, debug issues, and identify performance bottlenecks. **Do This:** """kotlin import timber.log.Timber //Add Timber to application class class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { //Only log in debug builds. Timber.plant(Timber.DebugTree()) } } } //Logging within the app. Timber.d("API Request: GET /users/123") Timber.i("API Response: %s", response.body()) Timber.e(e, "API Error") """ This coding standards document provides a comprehensive guide to API integration in Kotlin Android applications. Adhering to these standards will help developers build robust, maintainable, and secure applications that effectively interact with backend services and external APIs.