Native Android Development

SkillAI & models

Guides native Android development with Kotlin, Jetpack Compose, and Views interop. Use when building, rewriting, or reviewing modern Android apps after establishing runtime truth.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Native Android Development skill

What this skill tells your AI

The instructions your AI receives, as published by vasilyu1983/ai-agents-public in frameworks/shared-skills/skills/software-android-native/SKILL.md and read by ahel’s review.

Use this skill for native Android work only. It is the default shared-skill entrypoint for Compose-first Android apps targeting API 28+, bounded rewrites from older codebases, and agent-assisted workflows in Android Studio, Codex, and Claude Code.

Quick Reference

TaskDefault PicksNotes
State & UI
New UI screensJetpack ComposeViews interop only where existing mature flows or third-party SDKs require it
Observable stateViewModel + StateFlow (Kotlin 2.x)Replaces LiveData for new code
Async workKotlin Coroutines + FlowDispatchers.IO for blocking, Dispatchers.Default for CPU; structured concurrency preferred
Unit/integration testsJUnit 5 + TurbineTurbine for Flow testing; JUnit 5 for coroutine lifecycle
UI testsCompose Testing APIs (ComposeTestRule)Espresso only for Views interop or legacy screens
State machine discipline
Submit guardif (_uiState.value is Loading) returnPrevents double-tap duplicate submissions in ViewModel
Auto-reset transitionsviewModelScope.launch { delay(500); _uiState.value = Idle }Input ready for next action without manual UI reset
Minimal sealed classesRemove states that can't happen anymoreDead sealed subclasses produce dead when branches and mislead future readers
Networking & resilience
Network reachabilityConnectivityManager + NetworkCallback wrapped in StateFlowPublish isConnected; disable submit buttons when offline; observe in collectAsStateWithLifecycle
DI & architecture
Dependency injectionHilt@HiltViewModel, @Inject constructor, @Module + @InstallIn
Local persistenceRoom + KSPPrefer @Upsert over separate insert/update; KSP replaces KAPT
Background workWorkManager + CoroutineWorkerDeferrable, constraint-aware background processing
Agent tooling & build
Agent tooling (in Android Studio)Android Studio Gemini assistantBuilt-in coding agent surface
Agent tooling (outside IDE)Gradle CLI + ADBTerminal-first build, install, launch, and inspection
Build command./gradlew assembleDebugOr specific module: ./gradlew :app:assembleDebug
Install commandadb install -r app/build/outputs/apk/debug/app-debug.apk-r replaces existing without clearing data
Launch commandadb shell am start -n com.example.app/.MainActivityVerify package and component name from manifest
Emulator managementavdmanager, emulator CLIHeadless: emulator -avd Name -no-window -no-audio for CI
Logcatadb logcat -s TAG:VFilter by tag; adb logcat *:E for errors only
Screenshotadb exec-out screencap -p > screenshot.pngFast visual proof from emulator or device
Compose patterns
LazyColumn / LazyRowAlways provide key in items(key = { it.id })Prevents recomposition bugs on list mutation
Canvas drawingCanvas(modifier) { drawScope -> ... } with DrawScopeUse drawLine, drawCircle, drawArc, drawPath
Canvas gesturesModifier.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
AnimationsanimateFloatAsState, Animatable, InfiniteTransitionChoose based on one-shot vs continuous vs interruptible
derivedStateOfremember { derivedStateOf { ... } }For computed state that depends on frequently changing sources
Side effectsLaunchedEffect, DisposableEffect, SideEffectLaunchedEffect(key) for coroutine work; DisposableEffect for cleanup
Modifier orderPadding before background vs after changes resultModifier chain is sequential; order is layout-significant
Modifier.testTagModifier.testTag("submit_button")Required for Compose test node finders
SnackbarSnackbarHostState + SharedFlow from ViewModelCollect events in LaunchedEffect; never use Toast for important feedback
Billing & payments
BillingClientPlay 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 purchasesacknowledgePurchase() within 3 daysUnacknowledged purchases auto-refund after 3 days
Subscription offersProductDetails.subscriptionOfferDetailsBase plan, offer phases (free trial, introductory price)
Promotional offersDeveloper-determined offers in Play ConsoleConfigure offer eligibility; apply via BillingFlowParams.SubscriptionUpdateParams
ConsumablesconsumeAsync() after backend confirmsPrevents re-granting; consume only after server receipt
Adaptive layouts
Window size classesWindowSizeClass from material3-window-size-classCompact, Medium, Expanded; branch layout in Composable
List-detail paneListDetailPaneScaffold (Material3 adaptive)Canonical two-pane pattern for tablets and foldables
Navigation suiteNavigationSuiteScaffoldAuto-switches between bottom nav, rail, and drawer by size class
Foldable supportWindowInfoTracker (Jetpack Window)Detect fold posture, hinge bounds; adapt layout for table-top mode
Auth & push
Credential ManagerCredentialManager API (Jetpack)Unified passkeys, passwords, and federated sign-in
Biometric authBiometricPrompt (AndroidX)canAuthenticate() check first; BIOMETRIC_STRONG for crypto
Push notificationsFCM (FirebaseMessaging)onNewToken for registration; onMessageReceived for data messages
Notification channelsNotificationChannel (API 26+)Must create before posting; group related channels with NotificationChannelGroup
Deep linksCompose Navigation deep linksnavDeepLink { uriPattern = "app://..." } on route; App Links require assetlinks.json
collectAsStateWithLifecyclestateFlow.collectAsStateWithLifecycle()Lifecycle-aware collection; prevents updates when app is backgrounded
Strong Skipping (Kotlin 2.x)
UI state instance identitySplit state into @Immutable slices; hoist derived lists to ViewModelStrong Skipping Mode compares unstable params by reference; a fresh copy() per frame defeats skipping
LazyListScope lambdasval onClick = remember(id) { { vm.onClick(id) } }Lambda memoization from Strong Skipping only applies inside @Composablenot inside items { }
Main-thread UI mutationDo blocking work under withContext(Dispatchers.IO), assign _uiState.value = ... outside that blockOff-main state mutation surfaces as CalledFromWrongThreadException or ConcurrentModificationException in SnapshotStateObserver

When to Use This Skill

Use this skill to:

  • Build new Compose-first screens and features for Android apps targeting API 28+
  • Plan and execute bounded rewrites from Views or older Kotlin/Java codebases
  • Set up agent-assisted Android workflows in Android Studio, Codex, or Claude Code
  • Implement Kotlin Coroutines, Flow, and ViewModel state patterns
  • Prepare data safety declarations, target SDK compliance, and release gates
  • Review native Android code for architecture, performance, and compliance

Defaults

  • New native Android work: prefer Jetpack Compose for new screens and Views interop only where existing mature flows or third-party SDKs require it.
  • New observable UI state: prefer ViewModel + StateFlow and keep UI-facing state collected on the main thread with collectAsStateWithLifecycle.
  • Async work: prefer Kotlin Coroutines with structured concurrency; use Dispatchers.IO for blocking I/O and Dispatchers.Default for CPU-bound work.
  • Dependency injection: prefer Hilt for new projects.
  • Local persistence: prefer Room with KSP annotation processing.
  • Build system: prefer Gradle KTS (build.gradle.kts) with version catalogs (libs.versions.toml).
  • Navigation: prefer type-safe Compose Navigation 2.9+ with @Serializable route classes.
  • New unit and integration tests: prefer JUnit 5 with Turbine for Flow assertions.
  • UI tests: prefer Compose Testing APIs; keep Espresso for legacy Views screens.
  • Release gates: treat target SDK compliance, data safety declarations, ProGuard/R8 rules, Play Integrity, accessibility, and real-device verification as non-optional.

ASCII Flow

Android native task
  -> Confirm app shape: Compose, Views interop, service, or release gate
  -> Prove Gradle, emulator/device, install, and launch reality
  -> Choose architecture: ViewModel, StateFlow, Hilt, Room, Navigation
  -> Implement bounded slice with lifecycle-aware state and tests
  -> Check Kotlin, Compose, R8, billing, and Play-policy traps
  -> Build, install, launch, inspect logs, and report proof

Known Kotlin Traps

These are current headline footguns for Compose-first native Android on Kotlin 2.x. Each is source-backed; re-verify versions against the linked release notes before quoting a fix window.

  • CalledFromWrongThreadException / ConcurrentModificationException in SnapshotStateObserver. Compose reads snapshot state on the main thread; mutating _uiState.value from a coroutine body that ran on Dispatchers.IO without switching back surfaces as a main-thread crash or a reentrant-modification race. Fix: keep withContext(Dispatchers.IO) { ... } blocks pure (return a value, do not mutate state inside), assign _uiState.value = ... on the main dispatcher, and collect via collectAsStateWithLifecycle() — never a raw collect inside LaunchedEffect. See references/compose-state-concurrency.md.
  • Strong Skipping Mode identity checks. On Kotlin 2.x + Compose 1.8+, Strong Skipping Mode compares unstable params by instance reference. Emitting UI state as a fresh data class per event (_state.value = _state.value.copy(...)) defeats skipping even though the observable values are unchanged — LazyColumn rows recompose on every unrelated update. Fix: hoist derived lists and filtered views to the ViewModel with stateIn(scope, WhileSubscribed(5000), ...); split UI state into @Immutable slices; pass PersistentList<T> from kotlinx-collections-immutable. Source: developer.android.com/develop/ui/compose/performance/stability/strongskipping.
  • LazyListScope lambda memoization gap. Strong Skipping's automatic lambda memoization applies inside @Composable functions; it does not apply inside LazyListScope.items { ... } or LazyColumn's content block. Unremembered callbacks captured there produce a new instance per recomposition. Fix: val onClick = remember(id) { { vm.onClick(id) } } at the items-block site, or hoist to a stable () -> Unit reference.
  • Compose plugin version skew on Kotlin 2.x. Since Kotlin 2.0 the Compose compiler ships with the Kotlin compiler and is applied via the Gradle plugin kotlin("plugin.compose"). A stale or missing plugin declaration surfaces as Argument type mismatch: actual type 'Function0<Unit>', but '@Composable ComposableFunction0<Unit>' was expected — the transform did not run. Fix: lock plugin.compose to the exact Kotlin version in libs.versions.toml. Source: developer.android.com/jetpack/androidx/releases/compose-kotlin.
  • Compose runtime regressions fixed upstream. If you see a crash in pausable composition under LookaheadScope, nested Popup positioning against the screen instead of the parent, or a reentrant-modification crash in SnapshotStateObserver, upgrade to the latest Compose UI patch release before treating the problem as app-level — as of 2026-07-11 the production Compose BOM is in the 2026.06.xx line (Compose runtime/UI ~1.11.4, with 1.12 in beta requiring compileSdk 37 + AGP 9), and the SnapshotStateObserver reentrant-modification guard landed in the 1.10.0-rc01 cycle; a project still pinned below that line should treat this class of crash as a known-fixed upgrade target, not a fresh bug. Verify current at developer.android.com/jetpack/androidx/releases/compose-ui. Route to ../software-android-runtime-debugging/references/compose-debugging.md.
  • kotlinx-serialization + R8 full mode. Since kotlinx-serialization 1.9.0, AGP 8.x release builds can fail with SerializationException: Serializer for class 'X' is not found or ExceptionInInitializerError, and the build emits warnings about <1>$* keep rules. Fix: keep the generated $serializer classes explicitly and run a release-variant smoke test in CI that exercises every @Serializable entry point. Route to ../software-android-runtime-debugging/references/proguard-r8-triage.md.

Kotlin Anti-Patterns

These are behaviors to actively refuse in new code; they are not compile errors but each has caused measurable harm in production Kotlin/Android codebases.

#Anti-patternWhy it bitesBetter default
K1GlobalScope.launch { ... }Marked @DelicateCoroutinesApi; JetBrains is phasing it out. Coroutines started here survive navigation, never cancel, and accumulate as memory leaks on busy screens.viewModelScope, lifecycleScope, or an injected CoroutineScope parented to a SupervisorJob you own.
K2Passing an external Job into launch(externalJob) to "inherit" cancellationOverrides the scope's job, becomes the parent, and breaks structured concurrency. Cancellation of the scope no longer propagates. Recent IntelliJ releases flag this with a coroutine inspection.Never pass Job as a context argument. Use a child scope or a SupervisorJob explicitly scoped to the lifecycle you want.
K3LiveData + observeAsState in new Compose codeHidden main-thread hop, older backpressure, worse interaction with Strong Skipping because State<T> produced by observeAsState wraps a mutable holder.StateFlow + collectAsStateWithLifecycle() for new code. Keep LiveData only for legacy Views screens still on it.
K4Nullability as the primary way to model "loading" / "error" / "success"Forces every call site to branch on null and loses type information about why the value is absent.Sealed class / sealed interface: Idle / Loading / Success(data) / Error(message, cause) with an exhaustive when. Kotlin's compiler warns on missing branches when a new state is added.
K5Keeping kapt on Kotlin 2.x annotation processorskapt uses the old JVM backend and is often incompatible with K2; slows every build and can silently drop generated code.Migrate to KSP2 (K2-compatible). Hilt, Room, and Moshi-codegen all support KSP2; verify current support status in each library's release notes.
K6Treating StateFlow.value = copy(field = new) as freeStrong Skipping Mode compares the new object's reference to the old one. Even if only one primitive changed, consumers that take the whole state object as a parameter recompose.Split state into logical slices, hoist derived lists, and prefer primitives or @Immutable sub-objects as composable parameters.
K7Filtering or sorting lists inside a composable bodyCreates a new list reference per recomposition; Strong Skipping can never skip a downstream LazyColumn.Compute in ViewModel, expose as StateFlow<ImmutableList<T>>; or wrap in derivedStateOf { ... } inside a remember.
K8runBlocking { ... } in production code paths (outside main() and tests)Blocks the calling thread; on the main thread it freezes the UI and can ANR; in library code it defeats structured concurrency.Make the function suspend and let the caller pick the scope.

Route deeper pattern material through references/compose-state-concurrency.md.

Architecture Judgment Calls

Decisions that need a rationale, not just a default pick. As of 2026-07-11, verify each version-specific claim at the linked source before quoting it.

  • Compose vs Views in 2026. Compose is the default for all new screens; there is no scenario in a greenfield API 28+ app where Views is the right starting point. Keep Views only for: (1) a third-party SDK that ships a View-based render surface with no Compose wrapper (some map, ad, or video SDKs), (2) a legacy screen mid-migration where the cost of a full rewrite outweighs the interop tax, or (3) SurfaceView/TextureView-backed continuous rendering (camera preview, custom video) where Compose's AndroidView bridge is the right embedding, not a reason to avoid Compose for the rest of the screen. Do not accept "Views is faster" as a reason in 2026 — Compose's skip/restart model with Strong Skipping enabled is on par with or ahead of View-based RecyclerView diffing for list-heavy UI when state discipline (K6, K7) is followed.
  • Hilt vs Koin. Hilt (compile-time, annotation-processor-based, built on Dagger) remains the repo default for new API 28+ apps: it fails at compile time on a broken graph, has first-class @HiltViewModel / WorkManager / Compose Navigation integration, and is what most enterprise Android codebases already standardize on. Prefer Koin instead only when: the team explicitly wants to avoid annotation processing and Gradle plugin overhead (KSP-free build), the project is small enough that compile-time graph validation matters less than iteration speed, or the codebase is a Kotlin Multiplatform module where Hilt cannot run (Hilt is Android/JVM-only; Koin runs on all KMP targets). Do not switch an existing Hilt codebase to Koin mid-project without a concrete, named pain point — DI framework churn has a high cost for a marginal ergonomics gain.
  • Kotlin Multiplatform (KMP). KMP has been stable since November 2023 and Compose Multiplatform for iOS reached stable with 1.8.0; Jetpack libraries including Room, DataStore, and ViewModel now ship commonMain artifacts. This is a real option for sharing business logic (networking, persistence, ViewModel state) across Android and iOS — but it is a product/architecture decision, not a default for this skill. If the task is "should we share code with iOS," route to software-mobile (or the software-mobile-architect advisor, where available) for the cross-platform tradeoff call before writing shared-module code; this skill assumes the Android-native side once that call is made. Verify current KMP/Compose Multiplatform stability status at kotlinlang.org/docs/multiplatform/supported-platforms.html.
  • When NOT to go native. If the actual question is "should this feature be a native Android screen at all" (vs. a cross-platform framework, a web view, or a KMP-shared module), that decision belongs to software-mobile (or the software-mobile-architect advisor) — do not let this skill's Compose-first defaults silently answer a platform-choice question it was not asked.
  • Coroutines vs Flow failure modes. A suspend fun returns one value and is the wrong tool for anything that emits more than once (search-as-you-type, connectivity state, DB observation) — using a polling suspend loop instead of Flow produces stale reads and duplicate work. Conversely, wrapping a one-shot operation (a single network POST) in a Flow that a caller collects once adds Flow's cancellation/backpressure machinery for no benefit — a plain suspend fun is simpler and equally cancellable via structured concurrency. Rule of thumb: one value now -> suspend fun; zero-to-many values over time -> Flow; a single ViewModel-to-UI event stream that should not replay -> SharedFlow with replay = 0, not StateFlow.
  • Process death and SavedStateHandle. ViewModel survives configuration change but not process death under memory pressure. Anything the user would be upset to lose on a background-kill-and-restore (form input mid-fill, scroll position, in-progress multi-step flow state) must go through SavedStateHandle (@HiltViewModel constructor-injects it automatically), not just ViewModel field state. Test this with adb shell am kill <package> while backgrounded, not just rotation — rotation alone never exercises the process-death path and gives false confidence.

ANR and Frame Budget Arithmetic

Use these thresholds when diagnosing jank or ANR reports; re-derive the math rather than quoting a remembered number.

  • Input dispatch ANR: 5 seconds. If the main thread does not return from handling a touch or key event within 5s, the system raises Input dispatching timed out. This is the most common production ANR class and is almost always a synchronous DB/network/disk call on the main thread, not an actual 5-second-long computation.
  • Foreground service start ANR: 5 seconds. startForegroundService() must reach startForeground() within 5s or the system raises ForegroundServiceDidNotStartInTimeException territory (see S3 below for the related ForegroundServiceStartNotAllowedException/MissingForegroundServiceTypeException cases).
  • Broadcast receiver ANR: 10s foreground / 60s background. onReceive() running past this window on the relevant app-state timer raises an ANR; move any real work off onReceive() into WorkManager or a coroutine launched from a longer-lived scope.
  • Service execution ANR: 20s foreground / 200s background. onCreate()/onStartCommand()/onBind() blocking past this window raises an ANR; verify current thresholds at developer.android.com/topic/performance/anrs/diagnose-and-fix-anrs since these have shifted across Android versions.
  • Frame budget at 60Hz: 1000ms / 60 = 16.666...ms per frame. A composable recomposition, layout, and draw pass that together exceed ~16.67ms drops a frame; two consecutive misses read as visible jank.
  • Frame budget at 120Hz: 1000ms / 120 = 8.333...ms per frame. High-refresh-rate devices (now the majority of mid-range and flagship Android hardware) halve the available budget — a composable that was "fine" at 60Hz can visibly jank at 120Hz. Do not assume a perf fix validated at 60Hz on an emulator holds on a 120Hz physical device; re-verify with adb shell dumpsys gfxinfo <package> framestats or Android Studio's Frame Profiler on the actual refresh rate.

Runtime Truth And Prompting

Use a proof-first execution loop for native Android work:

  • verify tool reality first: Android Studio Gemini, Gradle CLI, ADB, and emulator or device availability
  • prove build, install, and launch before UI diagnosis
  • keep repo memory lean and fact-only
  • require bounded slices with explicit proof artifacts

Load references/runtime-proof-and-prompts.md for:

  • AI-agent defaults
  • proof-first and token-discipline rules
  • the execution loop
  • high-value prompt shape

Rewrite Workflow

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
87
Forks
19
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages (in references/agentic-android-tooling.md)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
software-android-native
Source
github.com/vasilyu1983/ai-agents-public