| State & UI | | |
| New UI screens | Jetpack Compose | Views interop only where existing mature flows or third-party SDKs require it |
| Observable state | ViewModel + StateFlow (Kotlin 2.x) | Replaces LiveData for new code |
| Async work | Kotlin Coroutines + Flow | Dispatchers.IO for blocking, Dispatchers.Default for CPU; structured concurrency preferred |
| Unit/integration tests | JUnit 5 + Turbine | Turbine for Flow testing; JUnit 5 for coroutine lifecycle |
| UI tests | Compose Testing APIs (ComposeTestRule) | Espresso only for Views interop or legacy screens |
| State machine discipline | | |
| Submit guard | if (_uiState.value is Loading) return | Prevents double-tap duplicate submissions in ViewModel |
| Auto-reset transitions | viewModelScope.launch { delay(500); _uiState.value = Idle } | Input ready for next action without manual UI reset |
| Minimal sealed classes | Remove states that can't happen anymore | Dead sealed subclasses produce dead when branches and mislead future readers |
| Networking & resilience | | |
| Network reachability | ConnectivityManager + NetworkCallback wrapped in StateFlow | Publish isConnected; disable submit buttons when offline; observe in collectAsStateWithLifecycle |
| DI & architecture | | |
| Dependency injection | Hilt | @HiltViewModel, @Inject constructor, @Module + @InstallIn |
| Local persistence | Room + KSP | Prefer @Upsert over separate insert/update; KSP replaces KAPT |
| Background work | WorkManager + CoroutineWorker | Deferrable, constraint-aware background processing |
| Agent tooling & build | | |
| Agent tooling (in Android Studio) | Android Studio Gemini assistant | Built-in coding agent surface |
| Agent tooling (outside IDE) | Gradle CLI + ADB | Terminal-first build, install, launch, and inspection |
| Build command | ./gradlew assembleDebug | Or specific module: ./gradlew :app:assembleDebug |
| Install command | adb install -r app/build/outputs/apk/debug/app-debug.apk | -r replaces existing without clearing data |
| Launch command | adb shell am start -n com.example.app/.MainActivity | Verify package and component name from manifest |
| Emulator management | avdmanager, emulator CLI | Headless: emulator -avd Name -no-window -no-audio for CI |
| Logcat | adb logcat -s TAG:V | Filter by tag; adb logcat *:E for errors only |
| Screenshot | adb exec-out screencap -p > screenshot.png | Fast visual proof from emulator or device |
| Compose patterns | | |
| LazyColumn / LazyRow | Always provide key in items(key = { it.id }) | Prevents recomposition bugs on list mutation |
| Canvas drawing | Canvas(modifier) { drawScope -> ... } with DrawScope | Use drawLine, drawCircle, drawArc, drawPath |
| Canvas gestures | Modifier.pointerInput(Unit) { detectTapGestures / detectDragGestures } | Compute hit targets from coordinates, not invisible tap areas |
| Type-safe navigation | @Serializable route classes + NavHost (Navigation 2.9+) | Compile-time route safety; replaces string-based routes |
| Animations | animateFloatAsState, Animatable, InfiniteTransition | Choose based on one-shot vs continuous vs interruptible |
derivedStateOf | remember { derivedStateOf { ... } } | For computed state that depends on frequently changing sources |
| Side effects | LaunchedEffect, DisposableEffect, SideEffect | LaunchedEffect(key) for coroutine work; DisposableEffect for cleanup |
| Modifier order | Padding before background vs after changes result | Modifier chain is sequential; order is layout-significant |
Modifier.testTag | Modifier.testTag("submit_button") | Required for Compose test node finders |
| Snackbar | SnackbarHostState + SharedFlow from ViewModel | Collect events in LaunchedEffect; never use Toast for important feedback |
| Billing & payments | | |
| BillingClient | Play Billing Library 8+ (v9.x current as of 2026-07-11; v8+ mandatory for all new apps/updates by 2026-08-31, extension to 2026-11-01) | Initialize in Application.onCreate or Hilt singleton; verify current minimum at developer.android.com/google/play/billing/release-notes |
| Acknowledge purchases | acknowledgePurchase() within 3 days | Unacknowledged purchases auto-refund after 3 days |
| Subscription offers | ProductDetails.subscriptionOfferDetails | Base plan, offer phases (free trial, introductory price) |
| Promotional offers | Developer-determined offers in Play Console | Configure offer eligibility; apply via BillingFlowParams.SubscriptionUpdateParams |
| Consumables | consumeAsync() after backend confirms | Prevents re-granting; consume only after server receipt |
| Adaptive layouts | | |
| Window size classes | WindowSizeClass from material3-window-size-class | Compact, Medium, Expanded; branch layout in Composable |
| List-detail pane | ListDetailPaneScaffold (Material3 adaptive) | Canonical two-pane pattern for tablets and foldables |
| Navigation suite | NavigationSuiteScaffold | Auto-switches between bottom nav, rail, and drawer by size class |
| Foldable support | WindowInfoTracker (Jetpack Window) | Detect fold posture, hinge bounds; adapt layout for table-top mode |
| Auth & push | | |
| Credential Manager | CredentialManager API (Jetpack) | Unified passkeys, passwords, and federated sign-in |
| Biometric auth | BiometricPrompt (AndroidX) | canAuthenticate() check first; BIOMETRIC_STRONG for crypto |
| Push notifications | FCM (FirebaseMessaging) | onNewToken for registration; onMessageReceived for data messages |
| Notification channels | NotificationChannel (API 26+) | Must create before posting; group related channels with NotificationChannelGroup |
| Deep links | Compose Navigation deep links | navDeepLink { uriPattern = "app://..." } on route; App Links require assetlinks.json |
collectAsStateWithLifecycle | stateFlow.collectAsStateWithLifecycle() | Lifecycle-aware collection; prevents updates when app is backgrounded |
| Strong Skipping (Kotlin 2.x) | | |
| UI state instance identity | Split state into @Immutable slices; hoist derived lists to ViewModel | Strong Skipping Mode compares unstable params by reference; a fresh copy() per frame defeats skipping |
LazyListScope lambdas | val onClick = remember(id) { { vm.onClick(id) } } | Lambda memoization from Strong Skipping only applies inside @Composable — not inside items { } |
| Main-thread UI mutation | Do blocking work under withContext(Dispatchers.IO), assign _uiState.value = ... outside that block | Off-main state mutation surfaces as CalledFromWrongThreadException or ConcurrentModificationException in SnapshotStateObserver |