# Security Best Practices Standards for Kotlin Android
This document outlines the security best practices for Kotlin Android development, providing guidelines for developers, informing architecture decisions, and serving as a reference for AI coding assistants. Adhering to these standards will minimize vulnerabilities, enhance application security, and ensure compliance with industry best practices.
## 1. Secure Data Storage
### 1.1 Encryption of Sensitive Data
**Standard:** All sensitive data must be encrypted at rest and in transit.
**Why:** Protecting data against unauthorized access in case of device compromise or data interception.
**Do This:**
* Utilize Android's "EncryptedSharedPreferences" or Jetpack Security library for encrypting data stored in shared preferences and files.
* Employ SQLCipher for encrypting SQLite databases.
* Use TLS/SSL for all network communications.
**Don't Do This:**
* Store sensitive data in plain text.
* Use hardcoded encryption keys.
* Rely on obfuscation as a primary security measure.
**Code Example: EncryptedSharedPreferences**
"""kotlin
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import android.content.Context
class SecureStorageManager(private val context: Context) {
private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
private val sharedPreferences = EncryptedSharedPreferences.create(
"secret_shared_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveSecret(secret: String) {
sharedPreferences.edit().putString("my_secret", secret).apply()
}
fun getSecret(): String? {
return sharedPreferences.getString("my_secret", null)
}
fun deleteSecret() {
sharedPreferences.edit().remove("my_secret").apply()
}
}
// Usage
val secureStorage = SecureStorageManager(context)
secureStorage.saveSecret("My Super Secret Value")
val retrievedSecret = secureStorage.getSecret()
println("Retrieved secret: $retrievedSecret")
secureStorage.deleteSecret()
"""
**Code Example: SQLCipher**
"""kotlin
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import android.content.Context
import android.content.ContentValues
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "my_secure_database.db"
private const val DATABASE_VERSION = 1
private const val PASSWORD = "my_secret_password" //Important: Manage password securely and NEVER hardcode it!!!
const val TABLE_NAME = "users"
const val COLUMN_ID = "_id"
const val COLUMN_USERNAME = "username"
const val COLUMN_PASSWORD = "password"
}
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY, $COLUMN_USERNAME TEXT, $COLUMN_PASSWORD TEXT)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
onCreate(db)
}
fun insertUser(username: String, password: String) {
writableDatabase.use { db ->
val values = ContentValues().apply {
put(COLUMN_USERNAME, username)
put(COLUMN_PASSWORD, password)
}
db.insert(TABLE_NAME, null, values)
}
}
fun readUsers():Unit {
readableDatabase.use {db ->
val cursor = db.query(TABLE_NAME, null, null, null, null, null, null)
with(cursor) {
while (moveToNext()) {
val userId = getInt(getColumnIndexOrThrow(COLUMN_ID))
val userName = getString(getColumnIndexOrThrow(COLUMN_USERNAME))
val password = getString(getColumnIndexOrThrow(COLUMN_PASSWORD))
println("User ID: $userId, User Name: $userName, Password: $password")
}
}
}
}
override fun getWritableDatabase(password: String?): SQLiteDatabase {
val db = super.getWritableDatabase(password)
return db
}
override fun getReadableDatabase(password: String?): SQLiteDatabase {
val db = super.getReadableDatabase(password)
return db
}
fun openDatabase() : SQLiteDatabase {
val database = this.writableDatabase
return database
}
fun closeDatabase(){
this.close()
}
}
//Initialize SQLCipher
fun initializeSQLCipher(context: Context){
SQLiteDatabase.loadLibs(context)
}
// Example usage
val helper = DatabaseHelper(context) //Get context correctly.
//Initialize SQLCipher libraries
initializeSQLCipher(context)
//Open the database
val db = helper.openDatabase()
//Insert User data
helper.insertUser("testUser", "password123")
//Read users
helper.readUsers()
//Close database
helper.closeDatabase()
"""
**Anti-Pattern:** Writing keys or passwords directly in the Kotlin/Java code or XML resource files. Keys, salts, and passwords should be managed safely outside of the app package (e.g., using the Android Keystore or a dedicated secrets management system).
### 1.2 Secure Key Management
**Standard:** Protect cryptographic keys using hardware-backed Keystore systems.
**Why:** Software-based key storage is vulnerable to attacks. The Keystore provides hardware-backed security that is resistant to key extraction.
**Do This:**
* Use the Android Keystore system to store cryptographic keys securely.
* Generate keys with strong entropy.
* Set appropriate key usage flags (e.g., encryption only, signing only).
**Don't Do This:**
* Hardcode keys in the application.
* Store keys in plain text files.
* Use weak or predictable key generation methods.
**Code Example: Android Keystore**
"""kotlin
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
class KeyStoreManager {
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
fun generateKey(keyAlias: String) {
if (!keyStore.containsAlias(keyAlias)) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(false) // Consider user authentication
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}
}
fun getKey(keyAlias: String): SecretKey? {
return keyStore.getKey(keyAlias, null) as? SecretKey
}
fun encryptionExample(plainText: String, keyAlias: String): ByteArray {
val key = getKey(keyAlias)
if(key == null){
throw Exception("Key not found")
}
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
val cipherText = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
return cipherText
}
fun decryptionExample(cipherText: ByteArray, keyAlias: String):String{
val key = getKey(keyAlias)
if(key == null){
throw Exception("Key not found")
}
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, key)
val plainText = cipher.doFinal(cipherText)
return String(plainText, Charsets.UTF_8)
}
}
// Usage example
val keyStoreManager = KeyStoreManager()
val keyAlias = "my_secret_key"
keyStoreManager.generateKey(keyAlias)
val plainText = "Sensitive data to encrypt"
var cipherText = keyStoreManager.encryptionExample(plainText, keyAlias)
println("Encrypted data: ${cipherText.contentToString()}")
val decryptedText = keyStoreManager.decryptionExample(cipherText, keyAlias)
println("Decrypted data: $decryptedText")
"""
**Anti-Pattern:** Using shared preferences as a substitute for the Android Keystore. This is insecure and should be avoided.
### 1.3 Removal of Sensitive Data
**Standard:** Ensure sensitive data is removed securely when no longer needed.
**Why:** Prevents data leakage if the device is compromised or when the application is uninstalled.
**Do This:**
* Overwrite memory with garbage data after use.
* Delete encrypted files securely. Implement standard secure deletion techniques when deleting files (e.g. overwriting).
* Clear shared preferences or database entries.
**Don't Do This:**
* Rely on standard file deletion methods alone.
* Assume data is automatically erased from memory.
* Leave temporary files containing sensitive information.
**Code Example: Secure File Deletion**
"""kotlin
import java.io.File
import java.io.FileOutputStream
fun secureDelete(file: File) {
if (file.exists()) {
val fileLength = file.length()
val random = java.security.SecureRandom()
FileOutputStream(file).use { fos ->
random.nextBytes(ByteArray(fileLength.toInt()))
fos.write(ByteArray(fileLength.toInt()))
}
if (!file.delete()) {
println("Failed to delete file") //Handle deletion errors gracefully. Potentially attempt re-deletion or flag for manual removal.
}
}
}
//Usage
val myFile = File(context.filesDir, "sensitive_data.txt")
myFile.writeText("This is sensitive.")
secureDelete(myFile)
"""
**Anti-Pattern:** Leaving sensitive data in logs, even if temporarily. Logging should be carefully reviewed for sensitive data and addressed proactively.
## 2. Secure Network Communication
### 2.1 HTTPS for All Network Requests
**Standard:** Use HTTPS for all network communication to protect data in transit using TLS/SSL.
**Why:** Prevents eavesdropping and man-in-the-middle attacks by encrypting data transmitted over the network.
**Do This:**
* Use "https://" URLs for all API endpoints.
* Validate SSL certificates to ensure the server is trusted.
* Configure network security policies to enforce HTTPS.
**Don't Do This:**
* Use HTTP ("http://") for sensitive data transmissions.
* Disable SSL certificate validation or ignore certificate errors.
* Allow insecure connections in production.
**Code Example: Enforcing HTTPS with Network Security Configuration**
Create "network_security_config.xml" in "res/xml":
"""xml
yourdomain.com
"""
Associate the config with your application in "AndroidManifest.xml":
"""xml
"""
**Code Example: Retrofit with HTTPS**
"""kotlin
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ApiClient {
private const val BASE_URL = "https://yourdomain.com/" // Ensure it's HTTPS
val instance: ApiInterface by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(ApiInterface::class.java)
}
}
"""
**Anti-Pattern:** Hardcoding network endpoints within the app for easy modification risks releasing an app pointing to a development or debugging endpoint in production.
### 2.2 Certificate Pinning
**Standard:** Implement certificate pinning to trust only specific certificates, mitigating man-in-the-middle attacks.
**Why:** Protects against compromised or fraudulent certificates used in MITM attacks.
**Do This:**
* Pin the server's certificate or public key in the app code.
* Update the pinned certificate regularly.
* Provide a backup pinning configuration in case of certificate rotation.
**Don't Do This:**
* Rely solely on the system's trusted certificate authorities.
* Hardcode certificates directly in the code without proper management.
* Skip certificate validation during pinning.
**Code Example: Certificate Pinning with OkHttp**
"""kotlin
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
object SecureOkHttpClient {
fun getUnsafeOkHttpClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add("yourdomain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // Replace with your domain and SHA256 pin
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
}
"""
**Anti-Pattern:** Ignoring updates to expiration dates of Certificates. Update mechanism should be robust to avoid hardcoded pins.
### 2.3 Input Validation
**Standard:** Validate all input received from external sources (network, intents, user input) to prevent injection attacks.
**Why:** Prevents malicious code from being injected into the application.
**Do This:**
* Use parameterized queries for database access to prevent SQL injection.
* Encode data appropriately before displaying it in web views.
* Sanitize user input before using it in external commands.
* Use Android's built-in input filters and regular expressions for validation.
**Don't Do This:**
* Trust user input without validation.
* Construct SQL queries by concatenating strings directly.
* Execute shell commands with user-provided input.
**Code Example: Input Validation using Regular Expressions**
"""kotlin
fun isValidEmail(email: String): Boolean {
val emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$".toRegex()
return emailRegex.matches(email)
}
// Usage
val email = "test@example.com"
if (isValidEmail(email)) {
println("Valid email")
} else {
println("Invalid email")
}
"""
**Anti-Pattern:** Neglecting to regularly update dependencies, because libraries with outdated versions sometimes contain security vulnerabilities.
## 3. Securing Application Components
### 3.1 Intent Handling
**Standard:** Use explicit intents for internal communication within the application and carefully validate data received from external intents.
**Why:** Prevents intent spoofing and data leakage by ensuring that only authorized components receive intents.
**Do This:**
* Use explicit intents when starting activities, services, and broadcast receivers within your app.
* Specify the target component by its fully qualified name.
* Apply permissions to restrict access to sensitive components.
* Validate and sanitize any data received from implicit intents.
**Don't Do This:**
* Use implicit intents for sensitive operations.
* Trust the source of incoming intents without validation.
* Expose sensitive data through exported components.
* Skip permissions for sensitive operations.
**Code Example: Explicit Intent**
"""kotlin
import android.content.Intent
import android.content.Context
fun startMyActivity(context: Context) {
val intent = Intent(context, MyActivity::class.java)
context.startActivity(intent)
}
"""
**Anti-Pattern:** Improper Use of Implicit Intents. Only use implicit intents when absolutely necessary (e.g., opening URL, sharing content) and always validate the receiving application.
### 3.2 Permissions Management
**Standard:** Request only the necessary permissions and protect sensitive operations with appropriate permissions.
**Why:** Minimizes the attack surface and reduces the risk of unauthorized access to sensitive data and functionality.
**Do This:**
* Request permissions at runtime when needed, using the Jetpack Compose Permissions library or Activity Result APIs.
* Use the principle of least privilege: only request the minimum permissions required.
* Protect sensitive application components with custom permissions.
**Don't Do This:**
* Request all permissions at install time.
* Grant unnecessary permissions to third-party libraries.
* Ignore permission checks before performing sensitive operations.
**Code Example: Runtime Permissions Request using Activity Result APIs**
"""kotlin
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.activity.result.ActivityResultLauncher
class MyFragment : Fragment() {
private lateinit var requestPermissionLauncher: ActivityResultLauncher
override fun onCreate() {
super.onCreate()
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow
// in your app.
} else {
// Explain to the user that the feature is unavailable because the
// features requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}
}
fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
// Provide an additional rationale to the user. This would happen
// if the user denied the request previously, but didn't check
// the "Don't ask again" check box.
// Show rationale UI to explain why permission is needed, then
// request the permission.
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
else -> {
// You can directly ask for the permission.
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
}
"""
**Anti-Pattern:** Over-permission, or requesting unnecessary permissions, erodes user trust and increases the attack surface potentially leading to permission abuse.
### 3.3 WebView Security
**Standard:** Properly configure WebView instances to prevent cross-site scripting (XSS) and other vulnerabilities.
**Why:** WebViews can be exploited to execute malicious JavaScript code, steal cookies, or access local resources if not configured correctly.
**Do This:**
* Disable JavaScript execution unless strictly necessary.
* Validate and sanitize any URLs loaded in the WebView.
* Use "setWebChromeClient" and "setWebViewClient" to handle JavaScript alerts and page navigation securely.
* Enable Safe Browsing to protect users from malicious websites.
**Don't Do This:**
* Enable JavaScript execution without proper input validation.
* Load untrusted or user-controlled URLs in the WebView.
* Expose sensitive data to JavaScript code running in the WebView.
* Ignore SSL certificate errors.
**Code Example: WebView Configuration**
"""kotlin
import android.webkit.WebView
import android.webkit.WebViewClient
fun configureWebView(webView: WebView) {
webView.settings.javaScriptEnabled = false // Disable JavaScript if not needed
webView.webViewClient = WebViewClient() // Handle page navigation
// Enable Safe Browsing
webView.settings.safeBrowsingEnabled = true
}
// Usage
val myWebView = WebView(context)
configureWebView(myWebView)
myWebView.loadUrl("https://www.example.com")
"""
**Anti-Pattern:** Enabling JavaScript in WebViews without thoroughly validating and sanitizing inputs can lead to severe XSS vulnerabilities and potential data breaches.
## 4. Code Security
### 4.1 Dependency Management
**Standard:** Update dependencies regularly and be aware of vulnerabilities in third-party libraries.
**Why:** Outdated and vulnerable third-party libraries are a common source of security vulnerabilities.
**Do This:**
* Use dependency management tools like Gradle with version catalogs to manage dependencies.
* Monitor dependencies for known vulnerabilities. Tools like Dependency-Check or Snyk integrate into CI/CD pipelines.
* Keep dependencies updated to the latest stable versions.
* Evaluate the security of third-party libraries before including them in the project.
**Don't Do This:**
* Use outdated or unsupported libraries.
* Ignore security warnings from dependency management tools.
* Blindly trust third-party code.
**Code Example: Gradle Dependency Management**
"""gradle
// build.gradle.kts (Module: app)
dependencies {
implementation("androidx.core:core-ktx:1.12.0") //Example
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.12.0") //Example
//Other dependencies...
}
"""
**Anti-Pattern:** Disregarding deprecation warnings can lead to unforeseen security issues as deprecated APIs are likely to have known vulnerabilities that may not be patched in older releases.
### 4.2 Secure Coding Practices
**Standard:** Follow secure coding practices to minimize vulnerabilities.
**Why:** Secure coding practices reduce the likelihood of introducing vulnerabilities such as buffer overflows, format string bugs, and race conditions.
**Do This:**
* Perform regular security code reviews.
* Use static analysis tools to identify potential vulnerabilities.
* Follow secure coding guidelines for Kotlin and Android.
* Implement proper error handling and logging.
**Don't Do This:**
* Ignore compiler warnings or static analysis results.
* Write complex or convoluted code that is difficult to understand and review.
* Expose sensitive data in error messages.
**Code Example: Safe Integer Handling**
"""kotlin
fun addSafely(a: Int, b: Int): Int? {
return try {
Math.addExact(a, b) //Throws ArithmeticException on overflow
} catch (e: ArithmeticException) {
null // or handle overflow appropriately
}
}
// Usage
val x = Int.MAX_VALUE
val y = 1
val sum = addSafely(x, y)
if (sum == null){
println("Integer overflow occurred!")
} else {
println("Sum: $sum")
}
"""
**Anti-Pattern:** Neglecting to handle exceptional cases gracefully can lead to denial-of-service (DoS) vulnerabilities if an attacker can trigger an unexpected error, and the application does not respond appropriately.
### 4.3 ProGuard/R8 Obfuscation
**Standard:** Use ProGuard or R8 to obfuscate the code, making it more difficult for attackers to reverse engineer the application.
**Why:** Obfuscation makes the code harder to understand and reverse engineer, hindering attackers attempting to analyze and exploit the application.
**Do This:**
* Enable ProGuard or R8 in the release build configuration.
* Configure ProGuard or R8 to remove unused code and rename classes and methods.
* Test the application thoroughly after enabling ProGuard or R8.
**Don't Do This:**
* Rely on obfuscation as the only security measure.
* Disable ProGuard or R8 in release builds.
* Exclude sensitive classes or methods from obfuscation.
**Code Example: Enabling R8 in "gradle.properties"**
"""
android.enableR8=true
"""
**Anti-Pattern:** Relying solely on obfuscation without addressing underlying vulnerabilities provides a false sense of security, as determined attackers can often bypass obfuscation through reverse engineering techniques.
By adhering to these security best practices, Kotlin Android developers can build robust and secure applications that protect sensitive data. These guidelines should be considered a living document, regularly updated to address evolving threats and emerging technologies.
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.
# 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<MainState> = _state.asStateFlow() //Expose for the view to render private val _effect: MutableSharedFlow<MainEffect> = 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> = _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 <T : ViewModel> create(modelClass: Class<T>): 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<AppState> = _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<AppState> = 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<String>() val myState: LiveData<String> = _myState init { // Retrieve the saved state, or use a default value _myState.value = savedStateHandle.get<String>("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<Preferences> by preferencesDataStore(name = "settings") // Example preferences keys object StoreKeys { val exampleCounter = intPreferencesKey("example_counter") } // Read the value val exampleCounterFlow: Flow<Int> = 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<String> = _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<String>("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.
# 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.