# Kotlin + Compose

Native Android apps with platform UI, lifecycle and storage integration.

Compose · ViewModel · Room · Coroutines

[Complete guide (Markdown)](/stacks/kotlin-compose-android/guide.md) · [Structured record (JSON)](/stacks/kotlin-compose-android/record.json)

Original source review: 2026-10-10. Core guidance rechecked: 2026-10-13. [Check scope](/research-freshness.json). Dependency combinations and setup need project-specific qualification.

## Use cases

- Android phones, tablets and foldables

- Native Android integrations

- Accessible mobile clients for a shared service

## Choose it when

- Android-native UI and platform behavior are requirements

- The team can own an Android SDK, emulator/device and signed release pipeline

## Consider another path when

- The requirement is a single shared UI for Android and iOS

- No Android device or emulator verification can be performed

## Working practices

- Expose immutable screen state from ViewModels and collect it with lifecycle awareness.

- Use lifecycle-owned coroutine scopes. Use WorkManager for eligible persistent, deferrable work.

- Test Room migrations and keep credential material protected by platform-backed keys.

- Check TalkBack, large text and real devices, then qualify the signed bundle through internal testing.

## Tools by job

### UI and state: Jetpack Compose + Material 3; screen ViewModels exposing immutable StateFlow with lifecycle-aware collection

Unidirectional data flow separates user events and rendered state; repositories own data access

ViewModels do not survive process death; persist important state and test restoration; add domain layers only for actual complexity

- [https://developer.android.com/topic/architecture/recommendations](https://developer.android.com/topic/architecture/recommendations)

### Structured local persistence: Room

Compile-time SQL checking, migrations and observable local data

Choose compatible Room/KSP/Kotlin versions; test upgrades; neither Room nor DataStore automatically supplies encryption or backend sync

- [https://developer.android.com/training/data-storage/room](https://developer.android.com/training/data-storage/room)

- [https://developer.android.com/topic/libraries/architecture/datastore](https://developer.android.com/topic/libraries/architecture/datastore)

### Preferences: DataStore

Compile-time SQL checking, migrations and observable local data

Choose compatible Room/KSP/Kotlin versions; test upgrades; neither Room nor DataStore automatically supplies encryption or backend sync

- [https://developer.android.com/training/data-storage/room](https://developer.android.com/training/data-storage/room)

- [https://developer.android.com/topic/libraries/architecture/datastore](https://developer.android.com/topic/libraries/architecture/datastore)

### Networking and API contract: Retrofit + OkHttp + kotlinx.serialization behind a repository

Typed HTTP interfaces with explicit DTOs and a replaceable client

Pin compatible versions and converter; share schemas/fixtures with iOS rather than UI; test auth refresh, errors and cancellation

- [https://github.com/lysine-dev/retrofit](https://github.com/lysine-dev/retrofit)

### Concurrency and background work: Coroutines/Flow in lifecycle-owned scopes; WorkManager for eligible persistent deferrable work

Cancellation follows lifecycle while scheduled local sync uses a persistent work system

WorkManager is OS-constrained and not an exact scheduler; coroutine launch alone is not durable across process death

- [https://developer.android.com/topic/architecture/recommendations](https://developer.android.com/topic/architecture/recommendations)

- [https://developer.android.com/develop/background-work/background-tasks/persistent](https://developer.android.com/develop/background-work/background-tasks/persistent)

### Credentials: Android Keystore-backed cryptographic keys protecting app-private credential material

Use OS key protection and explicitly scoped encrypted storage

Keystore stores keys, not arbitrary bearer tokens; define encryption, backup exclusions and revocation; no backend provider secrets in APKs

- [https://developer.android.com/privacy-and-security/keystore](https://developer.android.com/privacy-and-security/keystore)

### Unit and integration tests: JUnit + coroutine test utilities; fake repositories/API; Room migration tests

Deterministically verify state, retries, decoding, migrations and process-restoration assumptions

Host unit tests cannot prove Android framework/device behavior; emulator tests supplement them

- [https://developer.android.com/topic/architecture/recommendations](https://developer.android.com/topic/architecture/recommendations)

- [https://developer.android.com/training/data-storage/room](https://developer.android.com/training/data-storage/room)

### UI and accessibility: Compose UI tests + AndroidX accessibility checks; TalkBack and large-font/device review

Verify semantic actions, navigation and permission flows with the actual platform UI

Automatic checks require supported test dependencies/API environment and do not replace screen-reader usability; test tablets/foldables if supported

- [https://developer.android.com/develop/ui/compose/testing](https://developer.android.com/develop/ui/compose/testing)

- [https://developer.android.com/develop/ui/compose/accessibility/testing](https://developer.android.com/develop/ui/compose/accessibility/testing)

### Build, CI and distribution: Android Studio + Gradle wrapper/Kotlin DSL; pinned JDK/SDK CI; signed Android App Bundle and Play internal testing

Reproduce build inputs and separate local tests, device tests and signed release artifacts

AGP/Gradle/JDK/Kotlin/Compose must be compatible; release requires upload-key custody and Play account access; build success is not store publication

- [https://developer.android.com/build/building-cmdline](https://developer.android.com/build/building-cmdline)

- [https://developer.android.com/studio/publish/app-signing](https://developer.android.com/studio/publish/app-signing)

### Navigation: Navigation Compose

Navigation host/controller handle screen back stacks and deep links

Add compatible navigation-compose dependency; pass minimal route identifiers and test process recreation/back behavior. Preserve Fragment navigation during partial Compose migration.

- [https://developer.android.com/guide/navigation](https://developer.android.com/guide/navigation)

### Notifications: Android notification channels + runtime permission

Native notifications keep user delivery preferences under system control

Declare/request POST_NOTIFICATIONS where required; create appropriate channels, handle denial and test release API levels. Remote transport/server enrollment is a separate integration.

- [https://developer.android.com/develop/ui/compose/notifications/notification-permission](https://developer.android.com/develop/ui/compose/notifications/notification-permission)

### Media selection: Android Photo Picker

System picker grants access to selected media

Use Activity result contracts; handle fallback devices, URI access lifetime and upload size limits. Prefer selected access over requesting broad media-library permissions.

- [https://developer.android.com/training/data-storage/shared/photo-picker](https://developer.android.com/training/data-storage/shared/photo-picker)

Build with the stack’s strengths

## Architecture and developer workflow.

An installed Android client renders immutable screen state with lifecycle-aware collection and persists important data separately. Recommendation: repositories mediate Room/API data and WorkManager handles selected persistent work; backend services own privileged domain effects. Installed versions coexist through staged distribution.

Version scope: Documentation reviewed 11 October 2026. Use the project's minimum/target SDK, compatible Kotlin/Compose/AGP/Gradle/JDK/Room/WorkManager versions and version catalog. Current guidance is conditional on supported platform behavior; no APK, App Bundle, emulator or store release was tested. · Reviewed 2026-10-11

### Architecture boundaries

- Android guidance recommends separate UI and data layers, repositories, unidirectional data flow and lifecycle-aware Flow collection. Add a domain layer only when complexity/reuse warrants it.

- Recommendation: keep API DTOs distinct from Room entities, and local observable data authoritative for offline screen reads while the backend owns shared-domain reconciliation.

- Room owns schema versions and migrations; DataStore serves preferences rather than a general relational substitute. Record durable user operations and acknowledgement state when offline writes are required.

- Coroutines serve lifecycle/in-process async work; WorkManager persists scheduled work across restart/reboot and remains subject to constraints and power policy.

- Recommendation: use Keystore-backed keys to protect separately stored app-private credentials. A Keystore key is not arbitrary token storage, nor a guarantee that a revoked backend session remains usable.

### Tools and feedback

- Start at the external source README, API and database fixtures, version catalog and Gradle wrapper. Record package identity, SDK/device matrix, signing owner and release track.

- Use ./gradlew --version and ./gradlew tasks --all; with an actual app module run ./gradlew :app:lintDebug :app:testDebugUnitTest :app:assembleDebug. Keep wrapper/JDK/SDK inputs reproducible.

- After adb devices identifies an authorized emulator/device, run ./gradlew :app:connectedDebugAndroidTest. Configure Room's migration testing/exported schemas; assertions must check preserved data as well as schema shape.

- Recommendation: use Android Studio profilers/inspection, coroutine fakes, Compose navigation/accessibility tests and physical-device reboot/offline checks. Build ./gradlew :app:bundleRelease only with reviewed signing, then verify an internal-track install/update before staged promotion.

### Translations to avoid

- Collecting Flow without lifecycle ownership can keep work running when a screen is stopped; repeating effects inside recomposition can duplicate requests.

- Treating a ViewModel or coroutine as disk persistence loses work on process death.

- Using destructive migration for user-created offline data deletes the only copy.

- Expecting WorkManager to execute at an exact wall-clock deadline ignores scheduling windows and power constraints.

- Halt rollout is not uninstall or downgrade; already-updated users stay on the new build.

[developer.android.com →](https://developer.android.com/topic/architecture/recommendations)
[developer.android.com →](https://developer.android.com/topic/architecture/data-layer/offline-first)
[developer.android.com →](https://developer.android.com/training/data-storage/room/migrating-db-versions)
[developer.android.com →](https://developer.android.com/develop/background-work/background-tasks/persistent)
[developer.android.com →](https://developer.android.com/privacy-and-security/keystore)
[developer.android.com →](https://developer.android.com/build/building-cmdline)
[support.google.com →](https://support.google.com/googleplay/android-developer/answer/6346149)

For an existing application

## Keep the contracts while you change the stack.

These are documented options and proposed checks. Inspect the actual source application before adopting a transition.

Browser and installed clients

### Inspect the source

Supported installed APK versions/OS levels, DTOs/errors, auth, deep links and delayed old-client operations.

### During coexistence

Recommendation: Deploy backward-compatible server behavior first; run old/new clients against it and retain contracts through explicit adoption/support gates.

### Target design

Recommendation: Version API DTOs independently of Room entities and gate optional features by negotiated capability; keep backend authorization authoritative.

### Rollback

Disable the new capability and preserve old/new APIs; repair already-installed clients with a compatible higher-version release when needed.

### Benefits and prerequisites

**Client releases and backend changes can progress independently.**
Requires: Installed-version inventory, shared API fixtures and support ownership.
Tradeoffs: Maintaining old contracts costs work; Compose adoption does not upgrade installed clients.

### Checks before transfer

- Old/new builds decode missing/unknown fields and documented errors.

- A delayed old offline operation succeeds or returns the declared conflict.

- Deep links, session expiry and denied resources work across supported releases.

[developer.android.com →](https://developer.android.com/topic/architecture/recommendations)
[support.google.com →](https://support.google.com/googleplay/android-developer/answer/6346149)

Data and schema

### Inspect the source

Room schema versions, exported schemas, all upgrade paths and user-owned unsynced records.

### During coexistence

Recommendation: Add migrations without discarding old fields until adoption permits; include direct upgrades from every supported starting version.

### Target design

Recommendation: Keep exported Room schemas and explicit migration tests; use automatic or manual migrations according to the actual change.

### Rollback

Ship a forward repair when older code cannot read the new store; any restore requires reconciling writes made after upgrade.

### Benefits and prerequisites

**Reviewed migrations preserve local user data across updates.**
Requires: Complete paths plus assertions on data/relationships/pending work.
Tradeoffs: Schema validation alone does not establish value preservation; destructive fallback permanently deletes table data.

### Checks before transfer

- MigrationTestHelper exercises each path and earliest-to-current upgrade.

- Assert unsynced rows, IDs and relationships remain meaningful.

- Verify intended downgrade behavior; prohibit destructive fallback for the only copy of user work.

[developer.android.com →](https://developer.android.com/training/data-storage/room/migrating-db-versions)

Caching and shared state

### Inspect the source

Local source-of-truth reads, pending offline operations, revision/cursor/tombstone and conflict rules.

### During coexistence

Recommendation: Use one repository and one reconciler for each data category; persist queued mutations with operation IDs before reporting success.

### Target design

Recommendation: Use Room-backed local reads and a durable operation queue when offline writes are needed; select push/pull/hybrid sync and a domain-specific conflict policy.

### Rollback

Stop the new synchronizer and retain its queue; map remaining operations/cursors to old semantics before resume.

### Benefits and prerequisites

**Cached local data can keep required reads available and preserve offline intent.**
Requires: Server sync support, retry-safe writes and an explicit conflict policy.
Tradeoffs: Offline-first reads do not imply offline writes; last-write-wins can discard intent and depends on metadata semantics.

### Checks before transfer

- Two offline devices make conflicting changes and obtain the documented resolution.

- Kill process after local commit and before network send; operation survives.

- Repeat server-committed/unacknowledged mutation without duplication; honor deletes/tombstones.

[developer.android.com →](https://developer.android.com/topic/architecture/data-layer/offline-first)

Jobs and schedules

### Inspect the source

Existing queued Worker class names, input Data versions, unique-work identities and external effects.

### During coexistence

Recommendation: Retain readers/worker entry points for already-enqueued payloads; transfer one job category and prevent duplicate schedulers.

### Target design

Recommendation: Use WorkManager for eligible persistent device work with constraints/backoff; keep server completion obligations in a backend durable job.

### Rollback

Cancel new scheduling and account for pending/running work; keep compatibility workers until drained or safely transformed.

### Benefits and prerequisites

**Scheduled work persists across restart and device reboot.**
Requires: Compatible worker payloads, stable unique names, replay-safe effects and documented constraints.
Tradeoffs: Scheduling windows/power policy affect timing; unique work does not provide exactly-once remote effects.

### Checks before transfer

- Reboot with queued work and verify it resumes when constraints permit.

- Update app with old queued input; the new worker decodes it.

- Crash after remote effect/before acknowledgement and confirm retry is safe.

[developer.android.com →](https://developer.android.com/develop/background-work/background-tasks/persistent)
[developer.android.com →](https://developer.android.com/topic/architecture/data-layer/offline-first)

Identity and authorization

### Inspect the source

Existing stored credential encryption, key alias/access constraints, backup exclusions, issuer expiry/revocation.

### During coexistence

Recommendation: Retain the backend issuer and read old encrypted credential formats during a bounded transition; isolate account-local data.

### Target design

Recommendation: Use Keystore-backed cryptographic keys with reviewed encrypted app-private credential storage and backend revocation.

### Rollback

Retain compatibility decryption only while secure; require sign-in if the old format/key cannot be restored safely.

### Benefits and prerequisites

**OS key protection can reduce key extraction exposure.**
Requires: Qualified device/API support, key invalidation handling and a reviewed storage/backup policy.
Tradeoffs: Keystore stores keys rather than arbitrary tokens; device protection does not replace backend authorization.

### Checks before transfer

- Key invalidation produces a controlled sign-in path.

- Backup/restore cannot silently expose unusable/plain credentials.

- Sign-out/account switch stops old-account queued work and backend revocation is enforced.

[developer.android.com →](https://developer.android.com/privacy-and-security/keystore)

Delivery and recovery

### Inspect the source

Play tracks, signing/version codes, adoption, crash metrics and schema/API compatibility.

### During coexistence

Recommendation: Verify an internal/closed-track update over supported old builds and preserve compatible backend behavior for the whole mixed-version population.

### Target design

Recommendation: Use a staged rollout with explicit crash/data-loss thresholds and a higher-version compatible repair path.

### Rollback

Halt additional rollout and disable the affected capability if safe; publish a compatible fixed build without assuming a downgrade of installed clients.

### Benefits and prerequisites

**Percentage-based adoption can limit initial exposure.**
Requires: Signed build, account/track owner, telemetry and recovery artifact.
Tradeoffs: Halting only prevents further rollout; users who already updated remain on that version.

### Checks before transfer

- Install-over-old-build retains unsynced data and auth.

- Track failures by app/OS/schema version.

- Halt/recovery rehearsal keeps already-updated users functional.

[support.google.com →](https://support.google.com/googleplay/android-developer/answer/6346149)

[Read the migration workflow →](/rewrite/guide.md)

## Setup

- Start in a source checkout in a local development workspace. Record package ID, minimum/target SDK, supported form factors, API contract, permissions and release owner.

- Install Android Studio and compatible SDK/JDK; use the project Gradle wrapper. Create a Kotlin Empty Activity/Compose project. Pin Gradle/AGP/Kotlin/Compose and library versions in a version catalog.

- Implement screen state/ViewModels and repositories. Add Room migrations, a typed HTTP client and deterministic fake data. Keep secrets out of version control and app resources.

- Run JVM tests, lint and a debug build. Start a documented emulator or attach an authorized device, then run instrumentation/Compose checks.

- Exercise process death, offline/reconnect, rotation, permission denial, TalkBack, large fonts and each supported form factor on appropriate devices.

- Account owner establishes package ownership, upload key and Play App Signing. Build a signed release bundle, verify internal-track install/update, then use the authorized release workflow; retain reports and distribution receipts.

## Verification

Run these in the configured application; they are instructions, not recorded passes.

```
./gradlew --version
./gradlew tasks --all
./gradlew :app:lintDebug :app:testDebugUnitTest :app:assembleDebug
adb devices
./gradlew :app:connectedDebugAndroidTest
./gradlew :app:bundleRelease
```

## Open questions

- What minimum/target API and phone/tablet/foldable matrix must be supported?

- Which offline/sync/conflict rules and API authentication contract apply?

- Which notification/media features and denied-permission flows are required?

- Who owns package identity, upload key and Play internal/release tracks?

## Sources

- [Android architecture recommendations](https://developer.android.com/topic/architecture/recommendations)

- [Room persistence](https://developer.android.com/training/data-storage/room)

- [DataStore](https://developer.android.com/topic/libraries/architecture/datastore)

- [Retrofit maintained repository; square/retrofit redirects here](https://github.com/lysine-dev/retrofit)

- [Persistent background work](https://developer.android.com/develop/background-work/background-tasks/persistent)

- [Compose testing](https://developer.android.com/develop/ui/compose/testing)

- [Compose accessibility testing](https://developer.android.com/develop/ui/compose/accessibility/testing)

- [Android command-line build](https://developer.android.com/build/building-cmdline)

- [Android app signing](https://developer.android.com/studio/publish/app-signing)

- [Android Keystore system](https://developer.android.com/privacy-and-security/keystore)

- [Android Navigation](https://developer.android.com/guide/navigation)

- [Android notification permission](https://developer.android.com/develop/ui/compose/notifications/notification-permission)

- [Android Photo Picker](https://developer.android.com/training/data-storage/shared/photo-picker)

---

Canonical page: https://stack.menu/stacks/kotlin-compose-android/
