# Deployment and DevOps Standards for Kotlin Android
This document outlines the coding standards for Deployment and DevOps in Kotlin Android projects. It focuses on build processes, CI/CD pipelines, and production considerations, emphasizing best practices to ensure maintainability, performance, and security.
## 1. Build Processes
### 1.1 Gradle Configuration
* **Do This**: Use Kotlin DSL (".kts") for Gradle build files.
* **Why**: Kotlin DSL provides type safety, better IDE support, and improved readability compared to Groovy DSL.
"""kotlin
// build.gradle.kts (Module level)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}
android {
namespace = "com.example.myapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}
getByName("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
dataBinding = false
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Lifecycle
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}
"""
* **Don't Do This**: Use Groovy DSL (".gradle") for new projects or when migrating existing Gradle builds.
* **Why**: Kotlin DSL offers improved type safety, better autocompletion in IDEs, and is the modern standard.
### 1.2 Dependency Management
* **Do This**: Use dependency versions defined in the "gradle.properties" file or a dedicated versions catalog.
* **Why**: Centralizing dependency versions ensures consistency across the project, making updates easier and reducing errors.
"""kotlin
// gradle.properties
kotlin_version=1.9.22
appCompatVersion=1.6.1
coreKtxVersion=1.12.0
materialVersion=1.11.0
constraintLayoutVersion=2.1.4
junitVersion=4.13.2
testExtJunitVersion=1.1.5
espressoCoreVersion=3.5.1
lifecycleVersion=2.7.0
"""
"""kotlin
// build.gradle.kts
dependencies {
implementation("androidx.core:core-ktx:$coreKtxVersion")
implementation("androidx.appcompat:appcompat:$appCompatVersion")
}
"""
* **Do This**: Utilize Version Catalogs for more complex projects. This provides a type-safe way to manage dependencies.
"""kotlin
// libs.versions.toml
[versions]
agp = "8.4.0"
kotlin = "1.9.22"
core-ktx = "1.12.0"
appcompat = "1.6.1"
material = "1.11.0"
constraintlayout = "2.1.4"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
lifecycle = "2.7.0"
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinKapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
"""
"""kotlin
// build.gradle.kts
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.kotlinKapt)
}
dependencies {
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.runtime.ktx)
}
"""
* **Don't Do This**: Hardcode dependency versions directly in the "build.gradle.kts" file.
* **Why**: This leads to version conflicts and makes it difficult to update dependencies consistently.
### 1.3 Build Variants
* **Do This**: Define build variants to manage different app configurations for development, staging, and production environments.
* **Why**: Build variants allow you to tailor the app's behavior (e.g., API endpoints, logging levels) for each environment without modifying the source code.
"""kotlin
android {
buildTypes {
release {
buildConfigField("String", "API_URL", "\"https://api.example.com/prod/\"") // Example, do this with secrets
}
debug {
buildConfigField("String", "API_URL", "\"https://api.example.com/dev/\"") // Example, do this with secrets
}
}
flavorDimensions += "version"
productFlavors {
create("demo") {
dimension = "version"
applicationIdSuffix = ".demo"
versionNameSuffix = "-demo"
}
create("full") {
dimension = "version"
applicationIdSuffix = ".full"
}
}
}
"""
* **Do This**: Use "buildConfigField" to define environment-specific constants. Migrate towards secrets.
* **Why**: "buildConfigField" allows you to inject values into your code at compile time, ensuring that the correct configuration is used for each build.
"""kotlin
// Kotlin code
val apiUrl = BuildConfig.API_URL
val isFullVersion = BuildConfig.FLAVOR == "full"
"""
* **Don't Do This**: Use hardcoded values for environment-specific configurations directly in the code.
* **Why**: This makes it difficult to switch between environments and increases the risk of errors.
### 1.4 Signing Configuration
* **Do This**: Store signing configurations in a separate file outside the version control system for security.
* **Why**: Avoid committing sensitive information (like keystore passwords) to the repository.
* Use Gradle properties or environment variables for secure signing.
"""kotlin
// gradle.properties (or local.properties - exclude from VCS)
storePassword=your_store_password
keyPassword=your_key_password
"""
"""kotlin
// build.gradle.kts
signingConfigs {
create("release") {
storeFile = file("../keystore.jks")
storePassword = System.getenv("STORE_PASSWORD") ?: project.properties["storePassword"] as String? ?: "debug"
keyAlias = "your_key_alias" // Use secrets
keyPassword = System.getenv("KEY_PASSWORD") ?: project.properties["keyPassword"] as String? ?: "debug"
}
}
android {
signingConfigs {
getByName("release") {
storeFile = file("../keystore.jks")
storePassword = System.getenv("STORE_PASSWORD") ?: project.properties["storePassword"] as String? ?: "debug"
keyAlias = "your_key_alias" // Use secrets
keyPassword = System.getenv("KEY_PASSWORD") ?: project.properties["keyPassword"] as String? ?: "debug"
}
}
}
"""
* **Don't Do This**: Hardcode signing configurations directly in the "build.gradle.kts" file or commit them to the repository.
* **Why**: This exposes sensitive information and is a security risk.
### 1.5 Code Obfuscation and Optimization
* **Do This**: Enable ProGuard (or R8) for release builds to shrink, obfuscate, and optimize the code.
* **Why**: Reduces the APK size, making the app more difficult to reverse engineer and potentially improving performance.
"""kotlin
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true // Remove unused resources
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
"""
* **Do This**: Create and maintain "proguard-rules.pro" file.
* **Why**: Defines which classes and methods should be excluded from obfuscation and optimization (e.g., reflection-based code).
"""proguard
# Keep annotations
-keepattributes *Annotation*
# Keep classes used for reflection
-keep class com.example.myapp.MyClass {
*;
}
"""
* **Don't Do This**: Disable ProGuard/R8 for release builds.
* **Why**: Increases the APK size and makes the app more vulnerable to reverse engineering.
## 2. CI/CD
### 2.1 Automated Testing
* **Do This**: Integrate automated tests (unit, integration, and UI) into your CI/CD pipeline.
* **Why**: Ensures that code changes are thoroughly tested before being deployed, reducing the risk of regressions.
"""kotlin
// Example using GitHub Actions
name: Android CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Unit tests
run: ./gradlew testDebugUnitTest
- name: Run Instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
target: google_apis
arch: x86_64
profile: pixel_3a
emulator-cores: 2
script: ./gradlew connectedCheck
"""
* **Don't Do This**: Skip automated testing in the CI/CD pipeline.
* **Why**: This increases the risk of deploying untested code, potentially leading to bugs and instability in production.
### 2.2 Build Automation
* **Do This**: Use a CI/CD tool (e.g., Jenkins, CircleCI, GitHub Actions, GitLab CI) to automate the build, test, and deployment process.
* **Why**: Automates repetitive tasks, ensures consistency, and speeds up the delivery pipeline.
* **Do This**: Define clear build scripts and workflows.
* **Why**: Ensures that the build process is reproducible and consistent across different environments.
* **Do This**: Leverage Gradle's command-line interface for building and testing.
* **Why**: Provides a standardized way to interact with the build system.
"""bash
./gradlew assembleRelease
./gradlew testDebugUnitTest
./gradlew connectedCheck
"""
* **Don't Do This**: Manually build and deploy the app.
* **Why**: This is error-prone, time-consuming, and inconsistent.
### 2.3 Code Analysis
* **Do This**: Integrate static code analysis tools (e.g., detekt, ktlint, SonarQube) into your CI/CD pipeline.
* **Why**: Identifies potential issues in the code, such as code smells, bugs, and security vulnerabilities.
"""kotlin
// Example using detekt in build.gradle.kts
plugins {
id("io.gitlab.arturbosch.detekt").version("1.23.0")
}
detekt {
config = files("detekt.yml")
buildUponDefaultConfig = true
parallel = true
jvmTarget = "1.8"
}
tasks.detekt.configure {
reports {
html.required.set(true)
xml.required.set(true)
txt.required.set(false)
sarif.required.set(false)
}
}
"""
""" yml
// detekt.yml (Example for custom rules)
complexity:
LongMethod:
threshold: 20
style:
WildcardImport:
active: true
"""
* **Do This**: Integrate lint checks and enforce them in your CI/CD.
* **Why**: Ensures code adheres to Android's best practices and common coding standards.
"""kotlin
android {
lint {
abortOnError = true
checkAllWarnings = true
warningsAsErrors = true
xmlReport = true
htmlReport = true
}
}
"""
* **Don't Do This**: Ignore or disable code analysis tools.
* **Why**: This allows potential issues to go unnoticed, leading to technical debt and potential problems in production.
### 2.4 Deployment Strategies
* **Do This**: Implement appropriate deployment strategies, such as staged rollouts, canary releases, and A/B testing, to minimize the impact of potential issues.
* **Why**: Reduces the risk of deploying faulty code to all users at once and allows you to gather feedback and validate changes before a full rollout.
* **Do This**: Use Firebase App Distribution or similar services for beta testing.
* **Why**: Simplifies the distribution of beta versions to testers and allows you to gather feedback before releasing to production.
* **Don't Do This**: Deploy changes directly to production without any validation.
* **Why**: This is risky and can lead to widespread issues and user dissatisfaction.
### 2.5 Monitoring and Alerting
* **Do This**: Integrate monitoring tools (e.g., Firebase Crashlytics, Bugsnag, Sentry) to track crashes, exceptions, and performance metrics in production.
* **Why**: Provides insights into app behavior and allows you to identify and resolve issues quickly.
* **Do This**: Set up alerts for critical events (e.g., high crash rates, performance degradation).
* **Why**: Enables you to respond to issues promptly and prevent them from affecting a large number of users.
* **Don't Do This**: Ignore monitoring data and alerts.
* **Why**: This prevents you from identifying and resolving issues in a timely manner.
## 3. Production Considerations
### 3.1 Security
* **Do This**: Follow security best practices to protect sensitive data.
* **Why**: Prevents unauthorized access, data breaches, and other security incidents.
* **Do This**: Use the Jetpack Security library for encrypting sensitive data.
* **Why**: Provides a convenient and secure way to protect user data at rest.
"""kotlin
// Add the security-crypto dependency
implementation("androidx.security:security-crypto:1.1.0-alpha06") // Check latest version
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
val masterKeyAlias = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
"""
* **Do This**: Implement network security configurations to enforce TLS and prevent man-in-the-middle attacks.
* **Why**: Ensures that data transmitted over the network is protected from eavesdropping and tampering.
"""xml
api.example.com
"""
"""kotlin
android:networkSecurityConfig="@xml/network_security_config"
"""
* **Do This**: Use "SecretProvider" from Gradle Plugin 8.0 for storing API keys and sensitive information.
"""kotlin
// build.gradle.kts
android {
...
secrets {
defaultPropertiesFileName = "secrets.defaults.properties"
}
}
"""
"""properties
//secrets.defaults.properties
MAPS_API_KEY="AIza..." //NOT a real key
"""
"""kotlin
// Kotlin Code
val mapApiKey = BuildConfig.MAPS_API_KEY
"""
* **Don't Do This**: Hardcode sensitive information (e.g., API keys, passwords) in the code.
* **Why**: This exposes sensitive information and makes the app vulnerable to attacks.
### 3.2 Performance
* **Do This**: Optimize the app's performance to ensure a smooth and responsive user experience.
* **Why**: Improves user satisfaction and reduces the likelihood of negative reviews and uninstalls.
* **Do This**: Use profiling tools (e.g., Android Profiler) to identify performance bottlenecks and optimize the code.
* **Why**: Helps you find and fix performance issues quickly and efficiently.
* **Do This**: Implement lazy loading, caching, and other optimization techniques to reduce resource consumption and improve startup time.
* **Why**: Makes the app more responsive and efficient.
* **Don't Do This**: Ignore performance issues.
* **Why**: This can lead to a poor user experience and negatively impact the app's success.
### 3.3 Error Handling
* **Do This**: Implement robust error-handling mechanisms to gracefully handle unexpected errors and prevent crashes.
* **Why**: Improves the app's stability and prevents user frustration.
* **Do This**: Use "try-catch" blocks to handle potential exceptions.
* **Why**: Allows you to catch and handle exceptions without crashing the app.
"""kotlin
try {
// Code that may throw an exception
val result = 10 / 0
} catch (e: ArithmeticException) {
// Handle the exception
Log.e("Error", "Division by zero: ${e.message}")
//Report crashlytics/sentry
}
"""
* **Do This**: Use Kotlin's "Result" type for better error handling in functions.
"""kotlin
fun divide(a: Int, b: Int): Result {
return if (b == 0) {
Result.failure(IllegalArgumentException("Cannot divide by zero"))
} else {
Result.success(a / b)
}
}
fun main() {
val result = divide(10, 0)
result.fold(
onSuccess = { value -> println("Result: $value") },
onFailure = { error -> println("Error: ${error.message}") }
)
}
"""
* **Don't Do This**: Allow the app to crash without handling errors.
* **Why**: This is unprofessional and can damage the app's reputation
### 3.4 Logging
* **Do This**: Use appropriate logging levels (e.g., debug, info, warning, error) to provide valuable information about the app's behavior.
* **Why**: Helps you diagnose issues and understand how the app is being used.
* **Do This**: Use a logging framework (e.g., Timber) to simplify logging and customize the logging output.
* **Why**: Provides a consistent and flexible way to log information.
"""kotlin
// Install Timber
implementation("com.jakewharton.timber:timber:5.0.1") //Check latest
//Initiate in Application class
class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
// Usage:
Timber.d("This is a debug message")
Timber.i("This is an info message")
Timber.w("This is a warning message")
Timber.e("This is an error message")
"""
* **Don't Do This**: Log sensitive information (e.g., passwords, API keys) in production builds.
* **Why**: This exposes sensitive information and is a security risk. Also, avoid logging too much in production, as it can impact performance.
### 3.5 Feature Flags
* **Do This**: Use feature flags to enable or disable features remotely without deploying a new version of the app.
* **Why**: Allows you to roll out new features gradually, test them with a subset of users, and quickly disable them if any issues arise.
* **Do This**: Integrate a feature flag management platform (e.g., Firebase Remote Config, LaunchDarkly) into your app.
* **Why**: Provides a centralized and scalable way to manage feature flags.
"""kotlin
// Example using Firebase Remote Config (Conceptual)
val remoteConfig = Firebase.remoteConfig
fun isNewFeatureEnabled(): Boolean {
return remoteConfig.getBoolean("new_feature_enabled")
}
if (isNewFeatureEnabled()) {
// Show the new feature
} else {
// Hide the new feature
}
"""
* **Don't Do This**: Use hardcoded values to control feature availability.
* **Why**: This makes it difficult to enable or disable features remotely and requires a new app deployment.
This comprehensive document ensures that Kotlin Android projects adhere to high standards of build processes, CI/CD, and production considerations, promoting maintainability, performance, and security throughout the development lifecycle.
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'
# Tooling and Ecosystem Standards for Kotlin Android This document outlines coding standards focusing specifically on tooling and ecosystem-related best practices for Kotlin Android development. Following these standards ensures maintainable, performant, and secure applications built with the latest Kotlin Android features. ## 1. Dependency Management ### 1.1. Standard Utilize Gradle Kotlin DSL (build.gradle.kts) for dependency management. Leverage semantic versioning and Kotlin's type-safe accessors for dependencies. **Do This:** """kotlin // build.gradle.kts plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt") // For annotation processing (e.g., Dagger/Hilt) } android { namespace = "com.example.myapp" compileSdk = 34 defaultConfig { applicationId = "com.example.myapp" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } debug { // Debug-specific configurations go here } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 // Or a later version targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { viewBinding = true // Enable View Binding dataBinding = false // Explicitly disable Data Binding if not used } } dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.12.0-alpha04") implementation("androidx.constraintlayout:constraintlayout:2.1.4") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Kotlin Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // Retrofit for networking implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.retrofit2:converter-gson:2.11.0") // Dagger/Hilt for Dependency Injection implementation("com.google.dagger:hilt-android:2.51") kapt("com.google.dagger:hilt-compiler:2.51") // Lifecycle components implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0-alpha01") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0-alpha01") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.0-alpha01") // Timber for logging implementation("com.jakewharton.timber:timber:5.0.1") // Coil for image loading implementation("io.coil-kt:coil:2.6.0") // Room Persistence Library implementation("androidx.room:room-runtime:2.7.0") kapt("androidx.room:room-compiler:2.7.0") implementation("androidx.room:room-ktx:2.7.0") // Kotlin coroutine support for Room // Navigation Component implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") } """ **Don't Do This:** * Using the Groovy DSL ("build.gradle") for new projects. Kotlin DSL offers type safety and better IDE support. * Hardcoding dependency versions directly without using variables or dependency catalogs where possible. * Ignoring dependency updates. Regularly check for newer versions to benefit from bug fixes and performance improvements. **Why:** * **Maintainability:** Centralized dependency management simplifies version updates and ensures consistency across modules. * **Type Safety:** Kotlin DSL provides compile-time checking of dependencies, reducing runtime errors. * **Organization:** Well-structured "build.gradle.kts" files improve readability and ease collaboration. **Anti-pattern:** * Having inconsistent dependency versions across different modules. * Using a very old version of a support library without a valid reason. **Specific Notes for Kotlin Android:** Leverage Kotlin extensions (ktx) for Android libraries. For example, using "androidx.core:core-ktx" provides extension functions that make working with Android APIs more concise and Kotlin-friendly. ### 1.2. Dependency Catalogs Adopt dependency catalogs with TOML files to centralize and manage dependencies across multiple modules in larger projects. **Do This:** * Create a "libs.versions.toml" file in the "gradle" directory of your project. """toml # libs.versions.toml [versions] kotlin = "1.9.23" appcompat = "1.6.1" material = "1.12.0-alpha04" coroutines = "1.7.3" retrofit = "2.11.0" dagger = "2.51" lifecycle = "2.8.0-alpha01" room = "2.7.0" [libraries] androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.12.0" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } material = { module = "com.google.android.material:material", version.ref = "material" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } [plugins] androidApplication = { id = "com.android.application", version = "8.4.0" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinKapt = { id = "kotlin-kapt", version.ref = "kotlin" } daggerHiltPlugin = { id = "com.google.dagger.hilt.android", version = "2.51" } """ * Then, reference the libraries and plugins in your "build.gradle.kts" file: """kotlin plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlinKapt) alias(libs.plugins.daggerHiltPlugin) } android { // ... } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) implementation(libs.kotlinx.coroutines.android) implementation(libs.retrofit) implementation(libs.retrofit.converter.gson) implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.lifecycle.livedata.ktx) implementation(libs.room.runtime) kapt(libs.room.compiler) implementation(libs.room.ktx) } """ **Don't Do This:** * Ignoring the TOML file once created. Regularly update it as dependencies get updates. * Mixing versions defined in catalogs and direct dependencies defined in the build script. **Why:** * **Centralized version management:** Simplifies updating dependencies across all modules. * **Code reuse:** Avoids duplicate dependency declarations. * **Improved readability:** Makes the "build.gradle.kts" files cleaner and easier to understand. ## 2. Code Analysis and Linting ### 2.1. Standard Enable and configure static analysis tools like Android Lint, detekt, and Ktlint for automated code review. **Do This:** * Configure Android Lint in "build.gradle.kts": """kotlin android { lint { abortOnError = true // Fail build on lint errors checkAllWarnings = true // Treat all warnings as errors // Enable or disable specific lint checks disable("VectorDrawableCompat") enable("ObsoleteLayoutParam") } } """ * Integrate detekt for Kotlin code style and complexity analysis: """kotlin // Top-level build.gradle.kts plugins { id("io.gitlab.arturbosch.detekt").version("1.23.4") } // In your module's build.gradle.kts apply(plugin = "io.gitlab.arturbosch.detekt") detekt { config = files("detekt.yml") // Configuration file for detekt rules buildUponDefaultConfig = true } """ Create a "detekt.yml" file with custom rules and configurations: """yaml # detekt.yml complexity: LongParameterList: threshold: 5 ComplexMethod: threshold: 20 TooManyFunctions: threshold: 20 style: MaxLineLength: maxLineLength: 120 """ * Use Ktlint: Include Ktlint Gradle plugin. """kotlin plugins { id("org.jlleitschuh.gradle.ktlint").version("12.1.0") } """ * And then you can execute via gradle "ktlintFormat" and "ktlintCheck". And integrate this checks into your CI/CD pipeline **Don't Do This:** * Ignoring Lint warnings and errors. Treat them seriously and resolve them promptly. * Using default detekt configuration without customization. Adapt detekt rules to your project's needs. * Skipping code analysis in CI/CD pipelines. * Not using editorconfig files for consistent code style regarding indents and line endings between contributors. **Why:** * **Code Quality:** Static analysis tools identify potential bugs, code smells, and style violations early in the development process. * **Consistency:** Enforce coding standards across the entire codebase. * **Maintainability:** Reduce technical debt and simplify future code modifications. **Anti-pattern:** * Disabling useful lint checks without a valid reason. * Ignoring detekt findings and accumulating code quality issues. **Specific Notes for Kotlin Android:** Utilize Android Lint to detect Android-specific issues, such as performance problems in UI layouts or misuse of Android APIs. Customize Lint rules to match your project's requirements. Also utilize detekt rules related to Android to enforce best practices. ## 3. Asynchronous Programming and Concurrency ### 3.1. Standard Prefer Kotlin Coroutines for asynchronous programming in Android. Avoid traditional approaches like "AsyncTask" and "Thread". **Do This:** * Use "viewModelScope" or "lifecycleScope" for launching coroutines from "ViewModel" or "LifecycleOwner" components. """kotlin class MyViewModel : ViewModel() { private val _uiState = MutableLiveData<UiState>() val uiState: LiveData<UiState> = _uiState fun fetchData() { viewModelScope.launch { _uiState.value = UiState.Loading try { val data = withContext(Dispatchers.IO) { // Perform network request or other long-running operation apiService.getData() } _uiState.value = UiState.Success(data) } catch (e: Exception) { _uiState.value = UiState.Error(e) } } } } """ * Use "suspend" functions for asynchronous operations and "withContext" to switch dispatchers. """kotlin suspend fun performLongOperation(): Result<DataType> = withContext(Dispatchers.IO) { try { // Perform blocking I/O operation val result = ... Result.success(result) } catch (e: Exception) { Result.failure(e) } } """ * Handle asynchronous exceptions correctly using "try/catch" blocks within coroutines. * Use flows for handling streams of data over time. """kotlin val dataFlow: Flow<DataType> = flow { while (true) { val data = fetchDataFromSource() emit(data) delay(1000) // Emit data every 1 second } }.flowOn(Dispatchers.IO) """ **Don't Do This:** * Performing long-running operations on the main thread without using coroutines. * Ignoring cancellation signals in coroutines. * Blocking the main thread with "Thread.sleep" or similar constructs. * Using "AsyncTask" for new code. * Directly manipulating UI elements from background threads without proper context switching. **Why:** * **Simplicity:** Coroutines provide a more structured and readable way to handle asynchronous operations compared to callbacks or RxJava. * **Performance:** Coroutines are lightweight and efficient, reducing overhead compared to traditional threads. * **Structured Concurrency:** Coroutines simplify concurrent tasks through structured concurrency primitives (e.g., "async", "await"). **Anti-pattern:** * Launching coroutines without a defined scope (e.g., using "GlobalScope", which can lead to memory leaks). * Nested callbacks leading to "callback hell". **Specific Notes for Kotlin Android:** * Utilize "lifecycleScope" for Activity/Fragment specific coroutines, and "viewModelScope" for ViewModel-bound coroutines. * Wrap long-running operations in "withContext(Dispatchers.IO)" to avoid blocking the main thread ### 3.2. Executors and Thread Pools Be aware when using "Executors" and "Thread Pools". Explicitly manage lifecycles of long lived "Executors". Coroutines should be the preferred concurrency mechanism **Do This:** * Use "Executors.newFixedThreadPool(4)" for CPU-bound operations where more control than a Coroutine is desired. * Shutdown executor services when resources aren't required anymore """kotlin val executorService = Executors.newFixedThreadPool(4) fun submitTask(task: Runnable) { executorService.submit(task) } fun shutdown() { executorService.shutdown() try { if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { executorService.shutdownNow() } } catch (e: InterruptedException) { executorService.shutdownNow() } } """ **Don't Do This:** * Creating uncapped threads, as this could lead to resource exhaustion. ## 4. Testing ### 4.1. Standard Write comprehensive unit and integration tests using JUnit, Mockito/Mockk, and Espresso for UI testing. **Do This:** * Use JUnit for unit tests: """kotlin import org.junit.Assert.assertEquals import org.junit.Test class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } """ * Use Mockito or Mockk for mocking dependencies in unit tests: """kotlin import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.Test class MyClassTest { @Test fun testMyMethod() { val dependency = mockk<MyDependency>() every { dependency.doSomething() } returns "mocked result" val myClass = MyClass(dependency) val result = myClass.myMethod() assertEquals("mocked result", result) verify { dependency.doSomething() } } } """ * Use Espresso for UI tests: """kotlin import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click 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 org.junit.Rule import org.junit.Test class MainActivityTest { @get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java) @Test fun testButtonClick() { onView(withId(R.id.myButton)).perform(click()) onView(withId(R.id.myTextView)).check(matches(withText("Button Clicked!"))) } } """ * Use Turbine to test Kotlin flows. """kotlin import app.cash.turbine.test import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking import org.junit.Test import kotlin.time.Duration.Companion.seconds class ExampleFlowTest { @Test fun testFlowEmissions() = runBlocking { val flow = flow { emit(1) emit(2) emit(3) } flow.test(timeout = 2.seconds) { assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) assertEquals(3, awaitItem()) awaitComplete() } } } """ **Don't Do This:** * Skipping unit tests for critical logic. * Writing fragile UI tests that break easily due to minor UI changes. * Ignoring code coverage metrics. Aim for high code coverage to ensure thorough testing. * Relying solely on manual testing without automated tests. **Why:** * **Reliability:** Tests ensure that code behaves as expected and prevent regressions. * **Maintainability:** Tests make it easier to refactor and modify code without introducing bugs. * **Quality:** Tests improve the overall quality of the application. **Anti-pattern:** * Writing tests that are too complex or tightly coupled to implementation details. * Mocking everything instead of testing actual behavior. * Not using dependency injection, this leads to difficulties when mocking components. **Specific Notes for Kotlin Android:** * Use "TestCoroutineDispatcher" from "kotlinx-coroutines-test" to control the execution of coroutines in tests. * Use Hilt's testing APIs for integration testing of components that use dependency injection. ## 5. Logging and Monitoring ### 5.1. Standard Implement comprehensive logging using Timber and integrate with crash reporting tools like Firebase Crashlytics. **Do This:** * Use Timber for logging: """kotlin import timber.log.Timber class MyClass { fun myMethod(value: String) { Timber.d("myMethod called with value: %s", value) try { // Some code that may throw an exception } catch (e: Exception) { Timber.e(e, "Exception occurred in myMethod") } } init { if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } else { Timber.plant(CrashReportingTree()) } } } private class CrashReportingTree : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (priority == Log.ERROR || priority == Log.WARN) { // Send error logs to crash reporting service (e.g., Firebase Crashlytics) FirebaseCrashlytics.getInstance().log(message) t?.let { FirebaseCrashlytics.getInstance().recordException(it) } } } } """ * Integrate Firebase Crashlytics for crash reporting: 1. Add Firebase Crashlytics dependency in "build.gradle.kts": """kotlin dependencies { implementation("com.google.firebase:firebase-crashlytics-ktx:18.6.3") // Also requires Google Services plugin } """ 2. Upload the mapping file during builds process, this is configured through your build tooling * Use appropriate log levels (VERBOSE, DEBUG, INFO, WARN, ERROR) based on the severity of the message. * Log meaningful contextual information to help diagnose issues. **Don't Do This:** * Using "System.out.println" for logging. * Logging sensitive information (e.g., passwords, API keys) in production builds. * Ignoring crash reports and failing to address recurring issues. **Why:** * **Debugging:** Logs provide valuable insights into the behavior of the application and help identify the root cause of issues. * **Monitoring:** Crash reports enable proactive identification and resolution of crashes in production. * **Auditing:** Logs can be used to track user activity and ensure compliance with security and privacy policies. **Anti-pattern:** * Over-logging, which can impact performance and make it difficult to find relevant information. * Logging exceptions without including the exception stack trace. **Specific Notes for Kotlin Android:** * Plant different timber trees for debug and release mode. In debug mode you can see the logs in the console. Whereas in release mode data is sent to the crash reporting tool. * Use Timber's tag functionality to add context to log messages. ## 6. Proguard/R8 ### 6.1. Standard Enable Proguard or R8 for code shrinking, obfuscation, and optimization in release builds. **Do This:** * Enable Proguard/R8 in "build.gradle.kts": """kotlin android { buildTypes { release { isMinifyEnabled = true isShrinkResources = true //Remove unused resources proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } } """ * Create a "proguard-rules.pro" file to specify custom Proguard/R8 rules. """pro # Keep class names used in reflection -keep class com.example.myapp.MyReflectedClass { <methods>; <fields>; } # Keep class names used in JSON serialization/deserialization -keep class com.example.myapp.MyDataClass { <fields>; } # Keep entry points (Activities, Services, BroadcastReceivers, ContentProviders) -keep public class *.MyActivity -keep public class *.MyService -keep public class *.MyBroadcastReceiver -keep public class *.MyContentProvider # Keep View subclasses used in XML Layouts -keep public class * extends android.view.View { <init>(android.content.Context); <init>(android.content.Context, android.util.AttributeSet); <init>(android.content.Context, android.util.AttributeSet, int); public void set*(***); public *** get*(); } """ * Test release builds thoroughly after enabling Proguard/R8 to ensure that the application functions correctly. **Don't Do This:** * Disabling Proguard/R8 in release builds. * Using default Proguard/R8 configuration without customization. * Ignoring Proguard/R8 warnings and errors. **Why:** * **Security:** Obfuscation makes it more difficult for attackers to reverse engineer the application. * **Performance:** Code shrinking and optimization reduce the size of the APK and improve runtime performance. * **APK Size:** Removes unused code and resources. **Anti-pattern:** * Over-using "-keep" rules, which can reduce the effectiveness of code shrinking and obfuscation. * Not testing release builds before publishing to the Play Store. **Specific Notes for Kotlin Android:** * Keep Kotlin reflection metadata and coroutine debug metadata if needed. * Use "-keepnames" or "-keepclassmembers" to preserve class and member names used in reflection or data binding. * Always check for exceptions and crashes related to class not found exceptions in obfuscated builds ## 7. CI/CD Integration ### 7.1. Standard Integrate with CI/CD (Continuous Integration/Continuous Deployment) pipelines using tools like Jenkins, GitLab CI, GitHub Actions, or Bitrise. **Do This:** * Automate build, test, and deployment processes. """yaml # .github/workflows/android.yml name: Android CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle run: ./gradlew assembleRelease - name: Upload APK uses: actions/upload-artifact@v3 with: name: app-release.apk path: app/build/outputs/apk/release/app-release.apk """ * Configure CI/CD to run static analysis tools, unit tests, and UI tests. * Automate code signing and APK publishing to the Play Store. * Use environment variables for sensitive information (e.g., API keys, signing passwords). **Don't Do This:** * Manually building and deploying the application. * Skipping automated tests in CI/CD pipelines. * Committing sensitive information to the repository. **Why:** * **Automation:** CI/CD automates repetitive tasks, reducing manual effort and errors. * **Faster Feedback:** CI/CD provides rapid feedback on code changes, allowing developers to identify and fix issues quickly. * **Continuous Deployment:** CI/CD enables continuous deployment of new features and bug fixes to users. **Anti-pattern:** * Creating CI/CD pipelines that are too complex or difficult to maintain. * Not monitoring CI/CD pipeline performance and addressing bottlenecks. **Specific Notes for Kotlin Android:** * Use Gradle plugins for configuring CI/CD tasks. * Integrate with Firebase App Distribution for distributing beta builds to testers. * Use emulator snapshots in CI environment to save time for configuring emulator in each build step
# 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.
# 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 <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">yourdomain.com</domain> <!-- Replace with your domain --> </domain-config> </network-security-config> """ Associate the config with your application in "AndroidManifest.xml": """xml <application android:networkSecurityConfig="@xml/network_security_config" ...> </application> """ **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<String> 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.
# API Integration Standards for Kotlin Android This document outlines the coding standards and best practices for API integration in Kotlin Android applications. It aims to provide guidance for developers to build robust, maintainable, and performant Android applications that interact with backend services and external APIs. These standards are designed to be consistent with modern Kotlin and Android development approaches. ## 1. Architecture and Design ### 1.1. Layered Architecture **Standard:** Implement a layered architecture to separate concerns and improve maintainability. This typically includes: * **Data Layer:** Handles data retrieval and persistence (API calls, database interactions, etc.). * **Domain Layer:** Contains business logic and use cases. * **Presentation Layer (UI Layer):** Handles user interface and interaction logic (Activities, Fragments, Composable functions, ViewModels, etc.). * **Dependency Injection Layer (Optional but recommended):** Manages dependencies between layers. Use Hilt (recommended) or Koin. **Why:** Layered architecture promotes separation of concerns, making the codebase more modular and easier to test, maintain, and scale. **Do This:** """kotlin // Data Layer (Repository) class UserRepository(private val apiService: ApiService) { suspend fun getUser(userId: String): Result<User> { return try { val response = apiService.getUser(userId) if (response.isSuccessful) { Result.Success(response.body()!!) } else { Result.Error(Exception("API Error: ${response.code()}")) } } catch (e: Exception) { Result.Error(e) } } } // Domain Layer (UseCase) class GetUserUseCase(private val userRepository: UserRepository) { suspend operator fun invoke(userId: String): Result<User> { return userRepository.getUser(userId) } } // Presentation Layer (ViewModel) class UserViewModel( private val getUserUseCase: GetUserUseCase, savedStateHandle: SavedStateHandle // Use SavedStateHandle for persisting state across config changes ) : ViewModel() { private val _user = MutableStateFlow<Result<User>>(Result.Loading) val user: StateFlow<Result<User>> = _user.asStateFlow() val userId: String = savedStateHandle.get<String>("userId") ?: throw IllegalArgumentException("Missing user ID") init { loadUser() } private fun loadUser() { viewModelScope.launch { _user.value = getUserUseCase(userId) } } } """ **Don't Do This:** """kotlin // Anti-pattern: Mixing UI and API calls in the Activity/Fragment/Composable function class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Network call directly in the Activity! BAD! CoroutineScope(Dispatchers.Main).launch { val apiService = Retrofit.Builder() .baseUrl("https://example.com/api/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) try { val response = apiService.getUser("123") if (response.isSuccessful) { // Update UI } else { // Handle error } } catch (e: Exception) { // Handle exception } } } } """ ### 1.2. Data Transfer Objects (DTOs) **Standard:** Use DTOs to represent the data received from APIs. Map these DTOs to domain models within the data layer. **Why:** DTOs decouple your application's internal data models from the API's data structure. This allows you to adapt to API changes without affecting the entire application and improves testability. **Do This:** """kotlin // API Response DTO data class UserDto( val id: String, val username: String, val email: String ) // Domain Model data class User( val id: String, val username: String, val email: String ) // Data Layer (Repository) class UserRepository(private val apiService: ApiService) { suspend fun getUser(userId: String): Result<User> { return try { val response = apiService.getUser(userId) if (response.isSuccessful) { val userDto = response.body()!! val user = User(userDto.id, userDto.username, userDto.email) // Mapping DTO to Domain Model Result.Success(user) } else { Result.Error(Exception("API Error: ${response.code()}")) } } catch (e: Exception) { Result.Error(e) } } } """ **Don't Do This:** """kotlin // Anti-pattern: Using the API response class directly in the UI layer data class UserDto( //This becomes part of the UI layer's direct dependency val id: String, val username: String, val email: String ) // ViewModel class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _user = MutableLiveData<UserDto>() // Exposing DTO directly to the UI! BAD! val user: LiveData<UserDto> = _user fun loadUser(userId: String) { viewModelScope.launch { val result = userRepository.getUser(userId) if (result is Result.Success) { _user.value = result.data // Directly assigning API response to UI } else { // Handle error } } } } """ ### 1.3. Handling API Keys and Secrets **Standard:** Never hardcode API keys or secrets directly in your code. Use secure methods to store and access them, such as: * **Build Config Fields:** Use "buildConfigField" in your "build.gradle.kts" file to store API keys. These are compiled into the app and are more difficult to extract than resources. * **Secrets Gradle Plugin:** Use the "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" to securely manage secrets from a "secrets.properties" file during build time. * **Backend-for-Frontend (BFF):** Consider using a BFF to proxy API calls and manage secrets on the server side. This reduces exposure of secrets to the mobile client. **Why:** Hardcoding API keys compromises the security of your application and can lead to unauthorized access to your API accounts. **Do This:** """kotlin // build.gradle.kts (Module: app level) plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") //Secrets Gradle Plugin } android { buildTypes { release { buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") // Build config field // ... } debug { buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") // Build config field // ... } } //... } """ """kotlin //Accessing API Key (Kotlin) val apiKey = BuildConfig.API_KEY """ **secrets.properties:** """properties MAPS_API_KEY=YOUR_ACTUAL_MAPS_API_KEY """ **Don't Do This:** """kotlin // Anti-pattern: Hardcoding API Key val apiKey = "YOUR_API_KEY" // Never do this! """ ## 2. Retrofit and Networking ### 2.1. Retrofit Client **Standard:** Use Retrofit for making network requests. Configure the Retrofit client with appropriate settings, including: * **Base URL:** Define a constant for the base URL. * **Converter Factory:** Use "GsonConverterFactory" or "KotlinSerializationConverterFactory" (for Kotlinx Serialization). * **OkHttpClient:** Configure an "OkHttpClient" with timeouts, interceptors (for logging and authentication), and caching. * **Error Handling:** Use "try-catch" blocks or Kotlin's "Result" type to handle network errors gracefully. **Why:** Retrofit simplifies network requests, handles serialization/deserialization, and provides a clean, type-safe API. **Do This:** """kotlin import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object ApiClient { private const val BASE_URL = "https://api.example.com/" private val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // Log request and response details } private val okHttpClient = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .connectTimeout(30, TimeUnit.SECONDS) //Set timeouts .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() val retrofit: Retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) //Use Gson or Kotlinx Serialization .client(okHttpClient) .build() } // API Interface interface ApiService { @GET("users/{userId}") suspend fun getUser(@Path("userId") userId: String): retrofit2.Response<UserDto> } // Usage val apiService = ApiClient.retrofit.create(ApiService::class.java) """ **Don't Do This:** """kotlin // Anti-pattern: Creating a new Retrofit instance for every request fun getUser(userId: String): UserDto? { val retrofit = Retrofit.Builder() // Creating a new Retrofit instance every time! BAD! .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() val apiService = retrofit.create(ApiService::class.java) //... } """ ### 2.2. Coroutines and Flow **Standard:** Use Kotlin Coroutines and Flow for asynchronous network requests. Retrofit supports suspending functions, which can be called directly from coroutines. Use "flow" builder to emit data from the network layer to the UI. **Why:** Coroutines provide a concise and efficient way to handle asynchronous operations, making your code more readable and maintainable. Flow provides a reactive stream of data. **Do This:** """kotlin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.Dispatchers interface ApiService { @GET("users/{userId}") suspend fun getUser(@Path("userId") userId: String): Response<UserDto> } class UserRepository(private val apiService: ApiService) { fun getUserFlow(userId: String): Flow<Result<User>> = flow { emit(Result.Loading) // Emit Loading state try { val response = apiService.getUser(userId) if (response.isSuccessful) { val userDto = response.body()!! val user = User(userDto.id, userDto.username, userDto.email) emit(Result.Success(user)) // Emit Success with data } else { emit(Result.Error(Exception("API Error: ${response.code()}"))) //Emit Error } } catch (e: Exception) { emit(Result.Error(e)) //Emit Exception } }.flowOn(Dispatchers.IO) // Run on I/O thread } // ViewModel to collect the Flow class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _userState = MutableStateFlow<Result<User>>(Result.Loading) val userState: StateFlow<Result<User>> = _userState.asStateFlow() fun loadUser(userId: String) { viewModelScope.launch { userRepository.getUserFlow(userId) .collect { result -> _userState.value = result } } } } """ **Don't Do This:** """kotlin // Anti-pattern: Using callbacks for API calls fun getUser(userId: String, callback: (UserDto?) -> Unit) { // Avoid callbacks! val apiService = ApiClient.retrofit.create(ApiService::class.java) val call = apiService.getUser(userId) call.enqueue(object : Callback<UserDto> { override fun onResponse(call: Call<UserDto>, response: Response<UserDto>) { callback(response.body()) } override fun onFailure(call: Call<UserDto>, t: Throwable) { callback(null) } }) } """ ### 2.3. Error Handling with "Result" **Standard:** Use Kotlin's "Result" type (or a custom "Result" sealed class) to represent the outcome of API calls. This allows you to handle success, error, and loading states in a type-safe manner. **Why:** "Result" type forces you to handle both success and error scenarios preventing crashes and providing a better user experience. **Do This:** """kotlin // Define a Result sealed class if you're not targeting Android 13+ (which includes Result) sealed class Result<out T> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() object Loading : Result<Nothing>() } //Api Service & Repository (as shown in previous examples using Result type throughout) """ **Don't Do This:** """kotlin // Anti-pattern: Returning null on error fun getUser(userId: String): UserDto? { // Returning null on error!BAD! try { val response = apiService.getUser(userId) return response.body() } catch (e: Exception) { return null // Returning null } } """ ### 2.4. Pagination and List Handling **Standard:** Implement pagination when dealing with large datasets from APIs. Use appropriate query parameters to request data in chunks. Display a loading indicator while fetching more data. **Why:** Pagination improves performance and prevents your app from crashing due to excessive memory usage. It also provides a better user experience. **Do This:** """kotlin // API Interface interface ApiService { @GET("users") suspend fun getUsers( @Query("page") page: Int, @Query("limit") limit: Int ): Response<List<UserDto>> } // Repository class UserRepository(private val apiService: ApiService) { suspend fun getUsers(page: Int, limit: Int): Result<List<User>> { return try { val response = apiService.getUsers(page, limit) if (response.isSuccessful) { //... map to User objects } //... } //... } } // ViewModel (using Paging 3 library) class UserViewModel(private val userRepository: UserRepository) : ViewModel() { val userFlow: Flow<PagingData<User>> = Pager( config = PagingConfig(pageSize = 20, enablePlaceholders = false), pagingSourceFactory = { UserPagingSource(userRepository) } ).flow.cachedIn(viewModelScope) } // PagingSource class UserPagingSource( private val userRepository: UserRepository ) : PagingSource<Int, User>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> { val pageNumber = params.key ?: 1 val pageSize = params.loadSize return when (val result = userRepository.getUsers(pageNumber, pageSize)) { is Result.Success -> { val users = result.data LoadResult.Page( data = users, prevKey = if (pageNumber == 1) null else pageNumber - 1, nextKey = if (users.isEmpty()) null else pageNumber + 1 ) } is Result.Error -> { LoadResult.Error(result.exception) } Result.Loading -> { LoadResult.Error(Exception("Unexpected loading state")) // Handle loading within paging source is odd but possible } } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } } //In the UI use collectAsState(context = Dispatchers.IO.asExecutor()) to display paginated data """ **Don't Do This:** """kotlin // Anti-pattern: Fetching all data at once suspend fun getAllUsers(): List<UserDto> { // Fetching all users at once! BAD! val response = apiService.getAllUsers() return response.body() ?: emptyList() } """ ## 3. Data Caching and Offline Support ### 3.1. Caching Strategies **Standard:** Implement caching strategies to improve performance and provide offline support. Consider using: * **In-memory caching:** Cache frequently accessed data in memory using "LruCache" or similar mechanisms. * **Disk-based caching:** Use Room database to cache data persistently on disk. * **Network caching:** Use OkHttp's built-in caching mechanisms by configuring "Cache-Control" headers. **Why:** Caching reduces network requests, improves app responsiveness, and allows users to access data even when offline. **Do This:** """kotlin // OkHttp Cache Configuration val cacheSize = 10 * 1024 * 1024L // 10MB val cache = Cache(context.cacheDir, cacheSize) """ """kotlin // Add Interceptor for cache control val okHttpClient = OkHttpClient.Builder() .cache(cache) .addInterceptor { chain -> var request = chain.request() request = if (isNetworkAvailable(context)) request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build() // Cache for 1 minute when online else request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build() // Cache for 7 days when offline chain.proceed(request) } .build() // Function to check network availability (implement this based on ConnectivityManager) fun isNetworkAvailable(context: Context): Boolean { // Implementation... return false; } """ **Don't Do This:** """kotlin // Anti-pattern: Not implementing any caching suspend fun getUser(userId: String): UserDto? { // No caching! val response = apiService.getUser(userId) return response.body() } """ ### 3.2. Offline Data Synchronization **Standard:** Implement a mechanism to synchronize offline data with the backend when the device comes back online. Use a background service or WorkManager to handle synchronization. **Why:** Offline synchronization ensures data consistency and provides a seamless user experience even with intermittent network connectivity. **Do This:** """kotlin // WorkManager for syncing data in the background class SyncWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { private val userRepository = UserRepository(/*...*/); override suspend fun doWork(): Result { return try { val unsyncedData = // get unsynced data userRepository.syncData(unsyncedData) // Call API to sync Result.success() } catch (e: Exception) { Result.retry() // Retry the work later } finally { //Cleanup work } } } // Schedule the worker val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS) // Sync every hour .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "syncData", ExistingPeriodicWorkPolicy.KEEP, // Or REPLACE syncRequest ) """ ## 4. Security Considerations ### 4.1. HTTPS **Standard:** Always use HTTPS for all API communication to encrypt data in transit. **Why:** HTTPS protects sensitive data from eavesdropping and tampering. **Do This:** """kotlin // Correct: Using HTTPS private const val BASE_URL = "https://api.example.com/" // Use HTTPS """ **Don't Do This:** """kotlin // Incorrect: Using HTTP private const val BASE_URL = "http://api.example.com/" // Never use HTTP """ ### 4.2. Input Validation **Standard:** Validate all user input and API responses to prevent injection attacks and data corruption. **Why:** Input validation prevents malicious data from being processed by your application or the backend. **Do This:** """kotlin fun validateUserId(userId: String): Boolean { // Check if userId is valid (e.g., alphanumeric, length) return userId.matches(Regex("[a-zA-Z0-9]+")) && userId.length in 5..20 } """ ### 4.3. Data Encryption **Standard:** Encrypt sensitive data stored locally using appropriate encryption algorithms. **Why:** Data encryption protects data even if the device is compromised. ## 5. Testing ### 5.1. Unit Testing **Standard:** Write unit tests for your data layer (repositories, use cases) to verify that API calls are made correctly, data is parsed correctly, and errors are handled properly. Mock the "ApiService" using Mockito or Mockk. **Why:** Unit tests ensure the reliability of your API integration logic. **Do This:** """kotlin import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test class UserRepositoryTest { @Test fun "getUser should return success when API call is successful"() = runBlocking { // Arrange val mockApiService = mockk<ApiService>() // Create a Mock ApiService coEvery { mockApiService.getUser("123") } returns Response.success(UserDto("123", "test", "test@example.com")) // mock the result val userRepository = UserRepository(mockApiService) // Act val result = userRepository.getUser("123") // Assert assert(result is Result.Success) assertEquals("123", (result as Result.Success).data.id) } } """ ## 6. Monitoring and Logging ### 6.1. Logging **Standard:** Use a logging framework (Timber) to log API requests, responses, and errors. Log relevant information such as request URLs, response codes, and error messages. However, be very careful not to log sensitive data such as passwords or API keys. **Why:** Logging helps you monitor API usage, debug issues, and identify performance bottlenecks. **Do This:** """kotlin import timber.log.Timber //Add Timber to application class class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { //Only log in debug builds. Timber.plant(Timber.DebugTree()) } } } //Logging within the app. Timber.d("API Request: GET /users/123") Timber.i("API Response: %s", response.body()) Timber.e(e, "API Error") """ This coding standards document provides a comprehensive guide to API integration in Kotlin Android applications. Adhering to these standards will help developers build robust, maintainable, and secure applications that effectively interact with backend services and external APIs.
# Code Style and Conventions Standards for Kotlin Android This document outlines the code style and conventions standards for Kotlin Android development. Adhering to these guidelines will ensure code consistency, readability, maintainability, and optimal performance. It is designed to be used by developers and AI coding assistants to create high-quality Kotlin Android applications. ## 1. General Formatting Consistent formatting is crucial for code readability. Kotlin's syntax allows for flexibility, but adhering to standards helps maintain clarity. ### 1.1. Indentation and Whitespace * **Do This:** Use 4 spaces for indentation. Avoid tabs. Configure your IDE to automatically convert tabs to spaces. * **Don't Do This:** Rely on default IDE settings; they may not always match team conventions. """kotlin // Correct indentation fun calculateSum(a: Int, b: Int): Int { val sum = a + b return sum } // Incorrect indentation (using tabs or inconsistent spaces) fun calculateSum(a: Int, b: Int): Int { val sum = a + b return sum } """ * **Why This Matters:** Consistent indentation greatly enhances readability, making it easier to understand the code's structure and logic. * **Modern Approach:** Modern IDEs and linters can enforce indentation rules automatically. Use them! ### 1.2. Line Length * **Do This:** Limit lines to a maximum of 120 characters. This improves readability, especially on smaller screens. * **Don't Do This:** Allow lines to become excessively long, requiring horizontal scrolling. """kotlin // Good: Line length within limits val user = User( firstName = "John", lastName = "Doe", email = "john.doe@example.com" ) // Bad: Line exceeds limits (difficult to read) val user = User(firstName = "John", lastName = "Doe", email = "john.doe@example.com", address = "123 Main St, Anytown, USA") """ * **Why This Matters:** Readable code is easier to maintain and debug. Long lines reduce readability. * **Modern Approach:** Use automatic line wrapping features in your IDE to break long lines logically. ### 1.3. Vertical Whitespace * **Do This:** Use blank lines to separate logical blocks of code within functions or classes. Add blank lines between functions and classes. Use sparingly to avoid overly sparse code. * **Don't Do This:** Bunch all code together without any separation, or overuse blank lines. """kotlin // Good use of vertical whitespace fun processData(data: List<Int>): Int { // Filter out negative numbers val positiveData = data.filter { it > 0 } // Calculate the sum of positive numbers val sum = positiveData.sum() return sum } // Bad: No whitespace (hard to read) fun processData(data: List<Int>): Int {val positiveData = data.filter { it > 0 }val sum = positiveData.sum()return sum} """ * **Why This Matters:** Proper vertical whitespace improves visual structure, making code easier to scan and understand. ### 1.4. Braces * **Do This:** Use K&R style braces: opening brace on the same line as the statement, closing brace on its own line. * **Don't Do This:** Opening brace on the next line unless required by language semantics, inconsistent brace style. """kotlin // Correct brace style fun performAction(value: Int) { if (value > 0) { println("Positive value") } else { println("Non-positive value") } } """ * **Why This Matters:** Consistency in brace style improves readability and reduces visual clutter. ## 2. Naming Conventions Consistent and meaningful naming is essential for code maintainability and understandability. ### 2.1. General Naming * **Do This:** Use clear, descriptive names that accurately reflect the purpose of the variable, function, or class. * **Don't Do This:** Use single-letter variable names (except in loops), cryptic abbreviations, or names that don't reflect the purpose. * **Classes:** Use PascalCase (e.g., "UserManager", "DataParser"). * **Functions/Methods:** Use camelCase (e.g., "calculateTotal", "getUserDetails"). * **Variables:** Use camelCase (e.g., "userName", "orderTotal"). * **Constants:** Use UPPER_SNAKE_CASE (e.g., "MAX_VALUE", "DEFAULT_TIMEOUT"). * **Interfaces:** Use PascalCase, often prefixed with "I" (e.g., "IUserRepository"). (Note: While some prefer no "I" prefix, consistency within a project is paramount). """kotlin // Good naming class UserManager { fun getUserDetails(userId: String): User { // ... } companion object { const val MAX_USERS = 100 } } // Bad naming class UM { // cryptic fun gUD(id: String): User { //abbreviations // ... } companion object { const val MU = 100 // unclear } } """ * **Why This Matters:** Meaningful names improve code readability and reduce the cognitive load required to understand the code. * **Modern Approach:** Leverage IDE auto-completion to avoid typos when using long, descriptive names. ### 2.2. Android-Specific Naming * **Do This:** Follow Android's naming conventions for resources and UI elements. * **Don't Do This:** Use generic or confusing names for Android resources. * **Layout Files:** Use snake_case with a descriptive prefix (e.g., "activity_main.xml", "item_user_list.xml"). * **IDs for Views:** Use camelCase with a descriptive prefix based on View type (e.g., "userNameTextView", "submitButton"). * **Drawables:** Use snake_case with a descriptive prefix (e.g., "ic_launcher_background.xml", "button_background.xml"). """xml <!-- Good Naming --> <TextView android:id="@+id/userNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/submitButton" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <!-- Bad Naming --> <TextView android:id="@+id/tv1" // Undescriptive android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/btn1" // Undescriptive abbreviation android:layout_width="wrap_content" android:layout_height="wrap_content" /> """ * **Why This Matters:** Android's naming conventions ensure consistency across the project and ease collaboration. * **Modern Approach:** Use data binding and view binding to reduce boilerplate and make view references more type-safe, further emphasizing clear and descriptive IDs. ### 2.3. Package Naming * **Do This:** Use reverse domain name notation for package names (e.g., "com.example.myapp"). * **Don't Do This:** Use generic package names like "com.app" or "org.". """kotlin // Good package name package com.example.myapp.ui.home // Bad package name package com.app.ui.home // Not specific """ * **Why This Matters:** Package naming prevents naming conflicts and ensures unique identification of the application. ## 3. Stylistic Consistency and Best Practices Maintaining a consistent style improves readability and minimizes errors. ### 3.1. Null Safety * **Do This:** Utilize Kotlin's null safety features to prevent NullPointerExceptions ("?", "!!", "?:", "let", "run", "also", "apply"). Prefer safe calls ("?.") and elvis operator ("?:") over force unwrapping ("!!"). * **Don't Do This:** Rely on Java-style null checks. Overuse force unwrapping ("!!") without proper justification. """kotlin // Good: Safe calls and Elvis operator fun getUsername(user: User?): String { return user?.name ?: "Guest" // If user is null, return "Guest" } // Good: let block for null check and chained operations user?.let { it.address?.let { address -> println("User lives in ${address.city}") } } // Bad: Force unwrapping without null check (potential NPE) fun getUsername(user: User?): String { return user!!.name // Risky if user is null } // Bad: Java-style null check (less concise) fun getUsername(user: User?): String { if (user != null) { return user.name } else { return "Guest" } } """ * **Why This Matters:** NullPointerExceptions are a common source of errors in Android apps. Kotlin's null safety features provide a more robust and concise way to handle nullability. * **Modern Approach:** Consider using "@Nullable" and "@NonNull" annotations (from "androidx.annotation") for interoperability with Java code to improve nullability awareness. ### 3.2. Data Classes * **Do This:** Use data classes for classes that primarily hold data. * **Don't Do This:** Use regular classes for data-holding purposes, which require manual implementation of "equals()", "hashCode()", and "toString()". """kotlin // Good: Data class for representing user data data class User(val id: String, val name: String, val email: String) // Bad: Regular class (needs manual implementation of data-related methods) class UserClass(val id: String, val name: String, val email: String) { override fun equals(other: Any?): Boolean { // Manual implementation } override fun hashCode(): Int { // Manual implementation } override fun toString(): String { // Manual implementation } } """ * **Why This Matters:** Data classes automatically generate useful methods like "equals()", "hashCode()", "toString()", and "copy()", reducing boilerplate and improving code conciseness. * **Modern Approach:** Data classes are especially useful with Kotlin's destructuring declarations. ### 3.3. Immutability * **Do This:** Prefer immutable data structures (e.g., "val" properties, "List", "Set", "Map") over mutable ones ("var" properties, "MutableList", "MutableSet", "MutableMap") whenever possible. * **Don't Do This:** Unnecessarily use mutable data structures, leading to potential state management issues. """kotlin // Good: Immutable list val users: List<User> = listOf(User("1", "John", "john@example.com")) // Bad: Mutable list when immutability is sufficient var users: MutableList<User> = mutableListOf(User("1", "John", "john@example.com")) """ * **Why This Matters:** Immutable data structures simplify state management, reduce the risk of bugs, and improve thread safety. * **Modern Approach:** Use Kotlin's "copy()" function (available in data classes) to create modified copies of immutable objects. ### 3.4. Extension Functions * **Do This:** Use extension functions to add functionality to existing classes without modifying their source code. Use them judiciously to enhance code clarity and avoid polluting class APIs. * **Don't Do This:** Overuse extension functions, leading to code that is difficult to understand and maintain. """kotlin // Good: Extension function to validate email format fun String.isValidEmail(): Boolean { return android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches() } //Usage val email = "test@example.com" if(email.isValidEmail()) { println("Valid Email") } // Bad: Overusing extension functions for core class functionalities fun Context.showToast(message: String) { // Could simply use Toast.makeText inside the class Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } """ * **Why This Matters:** Extension functions promote code reusability and make code more expressive. They allow you to add utility functions to existing classes without inheritance or modification. * **Modern Approach:** Use extension functions to add functionality to Android framework classes (e.g., "Context", "View") for a more Kotlin-idiomatic way of interacting with the framework. ### 3.5. Coroutines * **Do This:** Utilize Kotlin Coroutines for asynchronous programming to avoid blocking the main thread. Use structured concurrency ("CoroutineScope") to manage coroutine lifecycles, especially in Android components (Activities, Fragments, ViewModels). Use "viewModelScope" (from "androidx.lifecycle:lifecycle-viewmodel-ktx") within ViewModels. * **Don't Do This:** Use "AsyncTask" for new code, or perform long-running operations directly on the main thread, leading to UI freezes. Abuse "GlobalScope" without proper lifecycle management. """kotlin // Good: Using viewModelScope for Coroutine lifecycle management class MyViewModel : ViewModel() { fun fetchData() { viewModelScope.launch { val data = withContext(Dispatchers.IO) { // Perform network request here on a background thread delay(1000) // Simulate network delay "Data from network" } // Update UI on the main thread _data.value = data } } private val _data = MutableLiveData<String>() val data: LiveData<String> = _data } // Bad: Blocking the main thread fun fetchData() { Thread.sleep(5000) // Simulate network call (BLOCKS UI) // Update UI (this will cause ANR if the delay is too long) } // Avoid: GlobalScope without lifecycle awareness fun fetchData() { GlobalScope.launch { // Potential memory leaks if not managed properly // ... } } """ * **Why This Matters:** Coroutines simplify asynchronous programming, improve app responsiveness, and prevent ANR (Application Not Responding) errors. Structured concurrency ensures proper resource management and avoids memory leaks. * **Modern Approach:** Use the "kotlinx.coroutines" library (included in most Android projects) and its integration with Android Architecture Components (like "viewModelScope") for streamlined asynchronous tasks. Use "Flow" for asynchronous streams of data. ### 3.6. Resource Management * **Do This:** Properly manage resources (e.g., file streams, network connections, database cursors) by closing them in "finally" blocks or using Kotlin's "use" function. "use" guarantees the resource is closed whether the code completes normally or throws an exception. * **Don't Do This:** Leak resources by not closing them, leading to performance issues or app crashes. """kotlin import java.io.BufferedReader import java.io.FileReader // Good: Using 'use' to automatically close the reader fun readFile(filePath: String): String? { return try { BufferedReader(FileReader(filePath)).use { reader -> reader.readText() } } catch (e: Exception) { e.printStackTrace() null } } // Bad: Not closing the reader (resource leak) fun readFileBad(filePath: String): String? { val reader = BufferedReader(FileReader(filePath)) return reader.readText() // Reader might not be closed, especially if an exception occurs. } """ * **Why This Matters:** Proper resource management prevents memory leaks, improves performance, and ensures app stability. * **Modern Approach:** Use "use" for any resource that implements the "Closeable" interface. ### 3.7. Dependency Injection * **Do This:** Use a dependency injection framework (e.g., Hilt, Dagger, Koin) to manage dependencies and improve testability. Hilt is the recommended dependency injection library for Android as of the latest versions. * **Don't Do This:** Manually create and manage dependencies throughout the codebase, leading to tightly coupled code and difficulty in testing. """kotlin // Good: Using Hilt for dependency injection (define module) @Module @InstallIn(SingletonComponent::class) object AppModule { @Singleton @Provides fun provideUserRepository(): UserRepository { return UserRepositoryImpl() } } // Good: Inject dependencies using @Inject class MyViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() { // ... } """ * **Why This Matters:** Dependency injection decouples components, making code more modular, testable, and maintainable. Hilt simplifies dependency injection in Android apps by providing a standard way to incorporate DI. * **Modern Approach:** Hilt streamlines DI setup and reduces boilerplate compared to Dagger. Consider Koin for smaller projects where simplicity is paramount. ### 3.8. View Binding and Data Binding * **Do This:** Prefer View Binding or Data Binding over "findViewById" for accessing views in layouts. Use Data Binding when two-way binding or complex UI logic is required. Use View Binding when only access to the view is needed. * **Don't Do This:** Use "findViewById" directly, as it's error-prone and requires manual casting. """kotlin // Good: Using View Binding private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.userNameTextView.text = "Hello, View Binding!" // Type-safe access } //Alternative Good: Using Data Binding private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.user = User("1", "DataBinding User", "") // Pass data to layout binding.lifecycleOwner = this // Let layout observe LiveData } """ """xml <!-- Example used with Data Binding --> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.myapp.User" /> </data> <TextView android:id="@+id/userNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name}" /> <!-- Use binding expression --> </layout> """ * **Why This Matters:** View Binding and Data Binding provide type-safe access to views, reduce boilerplate code, and improve performance, especially compared to "findViewById". Data Binding enables declarative UI updates and facilitates the Model-View-ViewModel (MVVM) architecture. * **Modern Approach:** Consider using Kotlin Android Extensions (view binding) plugin, but be aware that it is deprecated. Migration to ViewBinding is recommended. ### 3.9. Companion Objects * **Do This:** Use companion objects to hold constants, utility functions, or factory methods that are associated with a class but don't require an instance of the class. * **Don't Do This:** Overuse companion objects, especially for code that logically belongs to the class itself. """kotlin // Good: Companion object for constants class UserManager { companion object { const val MAX_USERS = 100 } fun createUser() { if (userCount < MAX_USERS) { // ... } } var userCount = 0 } """ * **Why This Matters:** Companion objects provide a clear and organized way to group related constants and functions within a class, improving code structure. ### 3.10. Sealed Classes * **Do This:** Use sealed classes to represent a limited set of possible subtypes or states. They're particularly useful for representing the states of a UI or the result of an operation. * **Don't Do This:** Use enums when sealed classes offer more flexibility (e.g., enums can't hold state). Use regular classes when sealed classes more accurately represent the problem domain. """kotlin // Good: Sealed class for representing result sealed class Result<out T : Any> { data class Success<out T : Any>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() object Loading : Result<Nothing>() } fun handleResult(result: Result<String>) { when (result) { is Result.Success -> println("Success: ${result.data}") is Result.Error -> println("Error: ${result.exception}") Result.Loading -> println("Loading...") } } """ * **Why This Matters:** Sealed classes enable exhaustive "when" statements, ensuring that all possible subtypes or states are handled. This improves code safety and maintainability. The compiler will warn you if you do not handle all possible subtypes in a "when" expression. ## 4. Kotlin Specific Features in Android ### 4.1. Use Scope Functions Appropriately * **Do This:** Understand the differences between "let", "run", "with", "apply", and "also", and use them accordingly to improve code readability and conciseness. * "let": Executes a block of code on a non-null object and returns the result of the block. Useful for null-safe operations and transforming data. * "run": Executes a block of code on an object and returns the result of the block. Similar to "let" but is called directly on the object: "obj.run { ... }". Useful for configuring data or performing calculations. * "with": Executes a block of code on an object (passed as an argument to "with") and returns the result of the block. Useful when you want to operate on the properties of an object within a concise scope: "with(obj) { ... }". * "apply": Executes a block of code on an object and returns the object itself. Useful for configuring an object and returning it in a fluent style. * "also": Executes a block of code with the object as an argument and returns the object itself. Useful for performing side effects (e.g., logging) without changing the object. * **Don't Do This:** Use them randomly or interchangeably, which can make the code confusing. """kotlin // Example using let for null-safe operation val name: String? = "John" val length = name?.let { println("Name is not null") it.length // Return value } ?: 0 // Example using apply for object configuration val textView = TextView(context).apply { text = "Hello" textSize = 16f setTextColor(Color.BLACK) } //Example using also for logging fun processData(data:String) : String { return data.also{ println("Data is $it") }.uppercase() } """ * **Why This Matters:** Scope functions allow writing more concise and expressive code, which can increase readability and reduce boilerplate. ### 4.2. Use Properties Instead of Getters and Setters * **Do This:** Prefer Kotlin's property syntax over explicit getter and setter methods. * **Don't Do This:** Create Java-style getter and setter methods unless you need custom logic within them. """kotlin // Good: Using properties class Person { var name: String = "" get() = field.uppercase() //backing field set(value) { field = value } } // Bad: Java-style getters and setters class PersonJavaStyle { private var name: String = "" fun getName(): String { return name } fun setName(name: String) { this.name = name } } """ * **Why This Matters:** Properties provide a more concise and Kotlin-idiomatic way to access and modify object state. ### 4.3. Use "when" Expressions * **Do This:** Prefer "when" expressions over "if-else" chains when handling multiple conditions, especially with sealed classes or enums. * **Don't Do This:** Use deeply nested "if-else" statements, which can be harder to read and maintain. """kotlin // Good: Using when expression fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long" else -> "Unknown" } """ ## 5. Performance Considerations ### 5.1. Avoid Unnecessary Object Creation * **Do This:** Minimize object creation, especially in performance-critical sections of code (e.g., inside loops or in "onDraw"). * **Don't Do This:** Create temporary objects unnecessarily, which can lead to increased memory consumption and garbage collection pauses. Use object pooling for frequently used objects. ### 5.2. Use Sparse Arrays * **Do This:** Use "SparseArray", "SparseBooleanArray", or "SparseIntArray" instead of "HashMap" for mapping integers to objects when memory efficiency is a concern, specifically when targeting older Android API levels. As of newer API levels, "HashMap" has become highly optimized and the performance differences have narrowed. * **Don't Do This:** Use "HashMap" blindly for integer keys, especially where memory is constrained. ### 5.3. Optimize Layouts * **Do This:** Optimize layouts by reducing the view hierarchy, using "ConstraintLayout" effectively, and avoiding overdraw. * **Don't Do This:** Create complex and deeply nested layouts, which can impact rendering performance. ## 6. Security Best Practices ### 6.1. Input Validation * **Do This:** Validate all user inputs to prevent injection attacks, buffer overflows, and other security vulnerabilities. Use parameterized queries for database interactions. * **Don't Do This:** Trust user inputs without validation. Construct SQL queries by concatenating strings. ### 6.2. Data Encryption * **Do This:** Encrypt sensitive data at rest and in transit. Use appropriate encryption algorithms and secure key management practices. Consider using Jetpack Security library. * **Don't Do This:** Store sensitive data in plain text. Hardcode encryption keys in the code. ### 6.3. Permissions * **Do This:** Request only the necessary permissions and explain why they are needed to the user. Request permissions at runtime when possible. Follow the principle of least privilege. * **Don't Do This:** Request all permissions upfront without justification. Store sensitive information without proper authorization. ### 6.4. Secure Coding Practices * **Do This:** Be aware of common security vulnerabilities (e.g., injection attacks, cross-site scripting, insecure data storage) and follow secure coding practices to mitigate them. Use static analysis tools to identify potential vulnerabilities. * **Don't Do This:** Ignore security warnings from IDEs or linters. Use deprecated or insecure APIs.