Kotlin + Compose
Native Android apps with platform UI, lifecycle and storage integration.
Compose · ViewModel · Room · Coroutines
Complete guide (Markdown) · Structured record (JSON)
Original source review: 2026-10-10. Core guidance rechecked: 2026-10-13. Check scope. 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
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
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
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
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
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
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
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
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
- Room persistence
- DataStore
- Retrofit maintained repository; square/retrofit redirects here
- Persistent background work
- Compose testing
- Compose accessibility testing
- Android command-line build
- Android app signing
- Android Keystore system
- Android Navigation
- Android notification permission
- Android Photo Picker