Before starting development, assess the current project state:
ScenarioCharacteristicsApproachEmpty DirectoryNo files presentFull initialization required, including Gradle WrapperHas Gradle Wrappergradlew and gradle/wrapper/ existUse ./gradlew directly for buildsAndroid Studio ProjectComplete project structure, may lack wrapperCheck wrapper, run gradle wrapper if neededIncomplete ProjectPartial files presentCheck missing files, complete configuration
Key Principles:
./gradlew assembleDebug succeedsgradle.properties is missing, create it first and configure AndroidXtext
MyApp/
├── gradle.properties # Configure AndroidX and other settings
├── settings.gradle.kts
├── build.gradle.kts # Root level
├── gradle/wrapper/
│ └── gradle-wrapper.properties
├── app/
│ ├── build.gradle.kts # Module level
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/example/myapp/
│ │ └── MainActivity.kt
│ └── res/
│ ├── values/
│ │ ├── strings.xml
│ │ ├── colors.xml
│ │ └── themes.xml
│ └── mipmap-*/ # App icons
properties
# Required configuration
android.useAndroidX=true
android.enableJetifier=true
# Build optimization
org.gradle.parallel=true
kotlin.code.style=official
# JVM memory settings (adjust based on project size)
# Small projects: 2048m, Medium: 4096m, Large: 8192m+
# org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
Note: If you encounter
OutOfMemoryErrorduring build, increase-Xmxvalue. Large projects with many dependencies may require 8GB or more.
kotlin
dependencies {
// Use BOM to manage Compose versions
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Activity & ViewModel
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
}
Product Flavors allow you to create different versions of your app (e.g., free/paid, dev/staging/prod).
Configuration in app/build.gradle.kts:
kotlin
android {
// Define flavor dimensions
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
// Different config values per flavor
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
// Different resources
resValue("string", "app_name", "MyApp Dev")
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
buildConfigField("String", "API_BASE_URL", "\"https://staging-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
resValue("string", "app_name", "MyApp Staging")
}
create("prod") {
dimension = "environment"
// No suffix for production
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "false")
resValue("string", "app_name", "MyApp")
}
}
buildTypes {
debug {
isDebuggable = true
isMinifyEnabled = false
}
release {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
Build Variant Naming: {flavor}{BuildType} → e.g., devDebug, prodRelease
Gradle Build Commands:
bash
# List all available build variants
./gradlew tasks --group="build"
# Build specific variant (flavor + buildType)
./gradlew assembleDevDebug # Dev flavor, Debug build
./gradlew assembleStagingDebug # Staging flavor, Debug build
./gradlew assembleProdRelease # Prod flavor, Release build
# Build all variants of a specific flavor
./gradlew assembleDev # All Dev variants (debug + release)
./gradlew assembleProd # All Prod variants
# Build all variants of a specific build type
./gradlew assembleDebug # All flavors, Debug build
./gradlew assembleRelease # All flavors, Release build
# Install specific variant to device
./gradlew installDevDebug
./gradlew installProdRelease
# Build and install in one command
./gradlew installDevDebug && adb shell am start -n com.example.myapp.dev/.MainActivity
Access BuildConfig in Code:
Note: Starting from AGP 8.0,
BuildConfigis no longer generated by default. You must explicitly enable it in yourbuild.gradle.kts:kotlin
android { buildFeatures { buildConfig = true } }
kotlin
// Use build config values in your code
val apiUrl = BuildConfig.API_BASE_URL
val isLoggingEnabled = BuildConfig.ENABLE_LOGGING
if (BuildConfig.DEBUG) {
// Debug-only code
}
Flavor-Specific Source Sets:
text
app/src/
├── main/ # Shared code for all flavors
├── dev/ # Dev-only code and resources
│ ├── java/
│ └── res/
├── staging/ # Staging-only code and resources
├── prod/ # Prod-only code and resources
├── debug/ # Debug build type code
└── release/ # Release build type code
Multiple Flavor Dimensions (e.g., environment + tier):
kotlin
android {
flavorDimensions += listOf("environment", "tier")
productFlavors {
create("dev") { dimension = "environment" }
create("prod") { dimension = "environment" }
create("free") { dimension = "tier" }
create("paid") { dimension = "tier" }
}
}
// Results in: devFreeDebug, devPaidDebug, prodFreeRelease, etc.
TypeConventionExampleClass/InterfacePascalCaseUserRepository, MainActivityFunction/VariablecamelCasegetUserName(), isLoadingConstantSCREAMING_SNAKEMAX_RETRY_COUNTPackagelowercasecom.example.myappComposablePascalCase@Composable fun UserCard()
Null Safety:
kotlin
// ❌ Avoid: Non-null assertion !! (may crash)
val name = user!!.name
// ✅ Recommended: Safe call + default value
val name = user?.name ?: "Unknown"
// ✅ Recommended: let handling
user?.let { processUser(it) }
Exception Handling:
kotlin
// ❌ Avoid: Random try-catch in business layer swallowing exceptions
fun loadData() {
try {
val data = api.fetch()
} catch (e: Exception) {
// Swallowing exception, hard to debug
}
}
// ✅ Recommended: Let exceptions propagate, handle at appropriate layer
suspend fun loadData(): Result<Data> {
return try {
Result.success(api.fetch())
} catch (e: Exception) {
Result.failure(e) // Wrap and return, let caller decide handling
}
}
// ✅ Recommended: Unified handling in ViewModel
viewModelScope.launch {
runCatching { repository.loadData() }
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message) }
}
Thread Selection Principles:
Operation TypeThreadDescriptionUI UpdatesDispatchers.MainUpdate View, State, LiveDataNetwork RequestsDispatchers.IOHTTP calls, API requestsFile I/ODispatchers.IOLocal storage, database operationsCompute IntensiveDispatchers.DefaultJSON parsing, sorting, encryption
Correct Usage:
kotlin
// In ViewModel
viewModelScope.launch {
// Default Main thread, can update UI State
_uiState.value = UiState.Loading
// Switch to IO thread for network request
val result = withContext(Dispatchers.IO) {
repository.fetchData()
}
// Automatically returns to Main thread, update UI
_uiState.value = UiState.Success(result)
}
// In Repository (suspend functions should be main-safe)
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
api.getData()
}
Common Mistakes:
kotlin
// ❌ Wrong: Updating UI on IO thread
viewModelScope.launch(Dispatchers.IO) {
val data = api.fetch()
_uiState.value = data // Crash or warning!
}
// ❌ Wrong: Executing time-consuming operation on Main thread
viewModelScope.launch {
val data = api.fetch() // Blocking main thread! ANR
}
// ✅ Correct: Fetch on IO, update on Main
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.fetch() }
_uiState.value = data
}
kotlin
// Default is public, declare explicitly when needed
class UserRepository { // public
private val cache = mutableMapOf<String, User>() // Visible only within class
internal fun clearCache() {} // Visible only within module
}
// data class properties are public by default, be careful when used across modules
data class User(
val id: String, // public
val name: String
)
kotlin
// ❌ Wrong: Accessing uninitialized lateinit
class MyViewModel : ViewModel() {
lateinit var data: String
fun process() = data.length // May crash
}
// ✅ Correct: Use nullable or default value
class MyViewModel : ViewModel() {
var data: String? = null
fun process() = data?.length ?: 0
}
// ❌ Wrong: Using return in lambda
list.forEach { item ->
if (item.isEmpty()) return // Returns from outer function!
}
// ✅ Correct: Use return@forEach
list.forEach { item ->
if (item.isEmpty()) return@forEach
}
kotlin
// ❌ Wrong: Fields declared as non-null (server may not return them)
data class UserResponse(
val id: String = "",
val name: String = "",
val avatar: String = ""
)
// ✅ Correct: All fields declared as nullable
data class UserResponse(
@SerializedName("id")
val id: String? = null,
@SerializedName("name")
val name: String? = null,
@SerializedName("avatar")
val avatar: String? = null
)
kotlin
// ❌ Wrong: Only adding Observer, not removing
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
// Memory leak!
}
// ✅ Correct: Paired add and remove
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
override fun onDetachedFromWindow() {
activity?.lifecycle?.removeObserver(this)
super.onDetachedFromWindow()
}
}
kotlin
import android.util.Log
// Info: Key checkpoints in normal flow
Log.i(TAG, "loadData: started, userId = $userId")
// Warning: Abnormal but recoverable situations
Log.w(TAG, "loadData: cache miss, fallback to network")
// Error: Failure/error situations
Log.e(TAG, "loadData failed: ${error.message}")
LevelUse Casei (Info)Normal flow, method entry, key parametersw (Warning)Recoverable exceptions, fallback handling, null returnse (Error)Request failures, caught exceptions, unrecoverable errors
kotlin
// ❌ Wrong: Calling Composable from non-Composable function
fun showError(message: String) {
Text(message) // Compile error!
}
// ✅ Correct: Mark as @Composable
@Composable
fun ErrorMessage(message: String) {
Text(message)
}
// ❌ Wrong: Using suspend outside LaunchedEffect
@Composable
fun MyScreen() {
val data = fetchData() // Error!
}
// ✅ Correct: Use LaunchedEffect
@Composable
fun MyScreen() {
var data by remember { mutableStateOf<Data?>(null) }
LaunchedEffect(Unit) {
data = fetchData()
}
}
kotlin
// Basic State
var count by remember { mutableStateOf(0) }
// Derived State (avoid redundant computation)
val isEven by remember { derivedStateOf { count % 2 == 0 } }
// Persist across recomposition (e.g., scroll position)
val scrollState = rememberScrollState()
// State in ViewModel
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
kotlin
// ❌ Wrong: Creating objects in Composable (created on every recomposition)
@Composable
fun MyScreen() {
val viewModel = MyViewModel() // Wrong!
}
// ✅ Correct: Use viewModel() or remember
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// ...
}
Must provide multi-resolution icons:
DirectorySizePurposemipmap-mdpi48x48Baselinemipmap-hdpi72x721.5xmipmap-xhdpi96x962xmipmap-xxhdpi144x1443xmipmap-xxxhdpi192x1924x
Recommended: Use Adaptive Icon (Android 8+):
xml
<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
TypePrefixExampleLayoutlayout_layout_main.xmlImageic_, img_, bg_ic_user.pngColorcolor_color_primaryString-app_name, btn_submit
Variable names, resource IDs, colors, icons, and XML elements must not use Android reserved words or system resource names. Using reserved names causes build errors or resource conflicts.
Common Reserved Names to Avoid:
CategoryReserved Names (Do NOT Use)Colorsbackground, foreground, transparent, white, blackIcons/Drawablesicon, logo, image, drawableViewsview, text, button, layout, containerAttributesid, name, type, style, theme, colorSystemapp, android, content, data, action
Examples:
xml
<!-- ❌ Wrong: Using reserved names -->
<color name="background">#FFFFFF</color>
<color name="icon">#000000</color>
<!-- ✅ Correct: Add prefix or specific naming -->
<color name="app_background">#FFFFFF</color>
<color name="icon_primary">#000000</color>
kotlin
// ❌ Wrong: Variable names conflict with system
val icon = R.drawable.my_icon
val background = Color.White
// ✅ Correct: Use descriptive names
val appIcon = R.drawable.my_icon
val screenBackground = Color.White
xml
<!-- ❌ Wrong: Drawable name conflicts -->
<ImageView android:src="@drawable/icon" />
<!-- ✅ Correct: Add prefix -->
<ImageView android:src="@drawable/ic_home" />
Error KeywordCauseFixUnresolved referenceMissing import or undefinedCheck imports, verify dependenciesType mismatchType incompatibilityCheck parameter types, add conversionCannot accessVisibility issueCheck public/private/internal@Composable invocationsComposable context errorEnsure caller is also @ComposableDuplicate classDependency conflictUse ./gradlew dependencies to investigateAAPT: errorResource file errorCheck XML syntax and resource references
./gradlew clean assembleDebugbash
# Clean and build
./gradlew clean assembleDebug
# View dependency tree (investigate conflicts)
./gradlew :app:dependencies
# View detailed errors
./gradlew assembleDebug --stacktrace
# Refresh dependencies
./gradlew --refresh-dependencies
Review Android UI files for compliance with Material Design 3 Guidelines and Android best practices.
PrincipleDescriptionPersonalDynamic color based on user preferences and wallpaperAdaptiveResponsive across all screen sizes and form factorsExpressiveBold colors and typography with personalityAccessibleInclusive design for all users
The latest evolution adds emotion-driven UX through:
Critical Decision: Match visual style to app category and target audience.
App CategoryVisual StyleKey CharacteristicsUtility/ToolMinimalistClean, efficient, neutral colorsFinance/BankingProfessional TrustConservative colors, security-focusedHealth/WellnessCalm & NaturalSoft colors, organic shapesKids (3-5)Playful SimpleBright colors, large targets (56dp+)Kids (6-12)Fun & EngagingVibrant, gamified feedbackSocial/EntertainmentExpressiveBrand-driven, gesture-richProductivityClean & FocusedMinimal, high contrastE-commerceConversion-focusedClear CTAs, scannable
See Design Style Guide for detailed style profiles.
ElementMinimum RatioBody text4.5:1Large text (18sp+)3:1UI components3:1
TypeSizeMinimum48 × 48dpRecommended (primary actions)56 × 56dpKids apps56dp+Spacing between targets8dp minimum
TokenValueUsagexs4dpIcon paddingsm8dpTight spacingmd16dpDefault paddinglg24dpSection spacingxl32dpLarge gapsxxl48dpScreen margins
CategorySizesDisplay57sp, 45sp, 36spHeadline32sp, 28sp, 24spTitle22sp, 16sp, 14spBody16sp, 14sp, 12spLabel14sp, 12sp, 11sp
TypeDurationMicro (ripples)50-100msShort (simple)100-200msMedium (expand/collapse)200-300msLong (complex)300-500ms
ComponentHeightMin WidthButton40dp64dpFAB56dp56dpText Field56dp280dpApp Bar64dp-Bottom Nav80dp-
TopicReferenceColors, Typography, Spacing, ShapesVisual DesignAnimation & TransitionsMotion SystemAccessibility GuidelinesAccessibilityLarge Screens & FoldablesAdaptive ScreensAndroid Vitals & PerformancePerformance & StabilityPrivacy & SecurityPrivacy & SecurityAudio, Video, NotificationsFunctional RequirementsApp Style by CategoryDesign Style Guide
Note: Only add test dependencies when the user explicitly asks for testing.
A well-tested Android app uses layered testing: fast local unit tests for logic, instrumentation tests for UI and integration, and Gradle Managed Devices to run emulators reproducibly on any machine — including CI.
Before adding test dependencies, inspect the project's existing versions to avoid conflicts:
gradle/libs.versions.toml — if present, add test deps using the project's version catalog stylebuild.gradle.kts for already-pinned dependency versionsVersion Alignment Rules:
Test DependencyMust Align WithHow to Checkkotlinx-coroutines-testProject's kotlinx-coroutines-core versionSearch for kotlinx-coroutines in build files or version catalogcompose-ui-test-junit4Project's Compose BOM or compose-compilerSearch for compose-bom or compose.compiler in build filesespresso-*All Espresso artifacts must use the same versionSearch for espresso in build filesandroidx.test:runner, rules, ext:junitShould use compatible AndroidX Test versionsSearch for androidx.test in build filesmockkMust support the project's Kotlin versionCheck kotlin version in root build.gradle.kts or version catalog
Dependencies Reference — add only the groups you need:
kotlin
dependencies {
// --- Local unit tests (src/test/) ---
testImplementation("junit:junit:<version>") // 4.13.2+
testImplementation("org.robolectric:robolectric:<version>") // 4.16.1+
testImplementation("io.mockk:mockk:<version>") // match Kotlin version
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>") // match coroutines-core
testImplementation("androidx.arch.core:core-testing:<version>") // InstantTaskExecutorRule for LiveData
testImplementation("app.cash.turbine:turbine:<version>") // Flow/StateFlow testing
// --- Instrumentation tests (src/androidTest/) ---
androidTestImplementation("androidx.test.ext:junit:<version>")
androidTestImplementation("androidx.test:runner:<version>")
androidTestImplementation("androidx.test:rules:<version>")
androidTestImplementation("androidx.test.espresso:espresso-core:<version>")
androidTestImplementation("androidx.test.espresso:espresso-contrib:<version>") // RecyclerView, Drawer
androidTestImplementation("androidx.test.espresso:espresso-intents:<version>") // Intent verification
androidTestImplementation("androidx.test.espresso:espresso-idling-resource:<version>")
androidTestImplementation("androidx.test.uiautomator:uiautomator:<version>")
// --- Compose UI tests (only if project uses Compose) ---
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // version from Compose BOM
debugImplementation("androidx.compose.ui:ui-test-manifest") // required for createComposeRule
}
Note: If the project uses a Compose BOM,
ui-test-junit4andui-test-manifestdon't need explicit versions — the BOM manages them.
Enable Robolectric resource support in the android block:
kotlin
android {
testOptions {
unitTests.isIncludeAndroidResources = true // required for Robolectric
}
}
LayerLocationRuns OnSpeedUse ForUnit (JUnit)src/test/JVMmsViewModels, repos, mappers, validatorsUnit + Robolectric100msCode needing Context, resources, SharedPrefsCompose UI (local)src/test/JVM + simulated Androidsrc/test/JVM + Robolectric100msComposable rendering & interactionEspressosecondsView-based UI flows, Intents, DB integrationCompose UI (device)src/androidTest/Device/Emulatorsrc/androidTest/Device/EmulatorsecondsFull Compose UI flows with real renderingUI AutomatorsecondsSystem dialogs, notifications, multi-appManaged Devicesrc/androidTest/Device/Emulatorsrc/androidTest/Gradle-managed AVD~minutes (first run)CI, matrix testing across API levels
See Testing for detailed examples, code patterns, and Gradle Managed Device configuration.
bash
# Local unit tests (fast, no emulator)
./gradlew test # all modules
./gradlew :app:testDebugUnitTest # app module, debug variant
# Single test class
./gradlew :app:testDebugUnitTest --tests "com.example.myapp.CounterViewModelTest"
# Instrumentation tests (requires device or managed device)
./gradlew connectedDebugAndroidTest # on connected device
./gradlew pixel6Api34DebugAndroidTest # on managed device
# Both together
./gradlew test connectedDebugAndroidTest
# Test with coverage report (JaCoCo)
./gradlew testDebugUnitTest jacocoTestReport