Android development / Basic: Android and Compose foundations / Lesson 6
Navigation, back stacks, and lifecycle
Design list-to-detail navigation while distinguishing screen presence, activity recreation, and process lifetime.
On this page
Lesson goals
- Model destinations with small stable arguments.
- Explain lifecycle transitions and choose state restoration boundaries.
Define destination contracts
Navigation is a user journey with a back stack, not merely swapping visible composables. A list destination opens a detail destination identified by a record id. Back returns through history; Up follows the app hierarchy, which can differ after an external entry. Keep destination arguments small and serializable, and load the actual record from the data layer. Missing records, invalid links, and unauthenticated entry need deliberate screen states. Sketch these cases before implementing navigation so the happy path does not become the only supported path.
Understand activity transitions
Android calls onCreate, onStart, onResume, onPause, onStop, and potentially onDestroy as activity circumstances change. Visibility and active interaction are related but distinct, especially in multi-window environments. Configuration changes can destroy and recreate an activity while the application process continues. Process death can remove all in-memory objects, and onDestroy is not guaranteed before that happens. Do not place the only copy of important user data in a callback or rely on a final cleanup method to persist it. Save confirmed edits through the data layer when the user commits them.
Pair observation with the visible owner
UI collection should respect lifecycle so an invisible screen does not keep unnecessary work active. In Compose, collectAsStateWithLifecycle is the usual Android flow collection bridge, provided by lifecycle-runtime-compose. In a view-based activity, repeatOnLifecycle can start and cancel a collection block around a lifecycle state. Restarting a collector must be safe: it should observe the current truth rather than repeat a purchase or another irreversible command. Lifecycle management controls observation; it does not transform a transient coroutine into durable background work.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.items.collect { items ->
adapter.submitList(items)
}
}
}
Place this once after adapter initialization in a ComponentActivity with lifecycle-runtime-ktx and coroutine imports. The example assumes a ViewModel exposing a Flow and a list adapter. A Compose screen should use its Compose collection bridge instead of creating this adapter layer.
Classify what must return
A ViewModel usually survives configuration recreation for its owner, but not process death. Save a small navigation id or draft through supported saved state and reload durable records from storage. Avoid saving entire result lists or bitmaps in a Bundle. Reopening an app after force-stop is a different scenario from restoring a system-recreated task, so define expectations explicitly. Test back navigation after direct entry and after recreation, and check that retry actions do not duplicate stack entries. A useful restoration test verifies the userβs task, not just the screen title.
Common errors and fixes
A detail route breaks for an id containing a slash
- Why it happens
- Raw data was concatenated into route syntax.
- How to fix it
- Encode the argument or use correctly configured typed navigation.
- Prevent it next time
- Test punctuation, Unicode, and missing arguments.
Saving only in onDestroy loses edits
- Why it happens
- Process termination does not guarantee that callback.
- How to fix it
- Persist committed changes when they occur and save small transient state separately.
- Prevent it next time
- Test restoration with the process genuinely recreated.
A hidden screen keeps collecting forever
- Why it happens
- Collection uses an unmanaged scope.
- How to fix it
- Use lifecycle-aware collection with the correct screen owner.
- Prevent it next time
- Review scope lifetime whenever adding a collector.
Check your understanding
1. A user opens a detail link whose record was deleted. Describe the destination behavior and back contract.
Show worked solution
Validate the id, attempt a repository lookup, and show a not-found state with a route to the list. Do not invent a blank record. Decide whether Back exits the externally entered task or returns through an existing stack, while Up offers the application hierarchy consistently.
2. A draft survives rotation but disappears after process death. Identify why and choose storage for draft text and confirmed records.
Show worked solution
The draft likely lived only in a ViewModel. Small transient text can use saved-state support if its restoration contract is sufficient; valuable long-lived drafts deserve durable storage. Confirmed records belong in the database. Save only the identity needed to reload the current record rather than duplicating its full data in saved state.
Key takeaways
- Back stacks express a journey with multiple entry paths.
- Configuration recreation and process death require different recovery mechanisms.
- Lifecycle-aware observation must be safe to restart.