Course outline

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.

About 4 min readContent reviewed
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.

Keep navigation at a clear boundary

The following excerpt uses the Navigation Compose NavHost API with a stable string route to make the stack visible. It is a specific supported approach, not a claim that every navigation library uses the same API. Pin a compatible navigation-compose dependency; typed routes require their documented serialization setup, and Navigation 3 has a different state model. Pass callbacks into screens instead of giving every component the controller. This lets list and detail UI be previewed and tested without constructing a whole navigation host.

kotlin A Navigation Compose host for two existing screens
@Composable
fun ReadingNavigation() {
    val nav = rememberNavController()
    NavHost(navController = nav, startDestination = "list") {
        composable("list") {
            ListScreen(onOpen = { id -> nav.navigate("detail/${Uri.encode(id)}") })
        }
        composable("detail/{id}", arguments = listOf(
            navArgument("id") { type = NavType.StringType }
        )) { entry ->
            DetailScreen(id = entry.arguments?.getString("id"))
        }
    }
}

Import androidx.navigation.compose APIs, NavType, navArgument, and android.net.Uri. ListScreen and DetailScreen are app-defined composables; DetailScreen accepts a nullable id and handles invalid input. URI encoding prevents path punctuation from changing route structure.

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.

kotlin Lifecycle-aware collection in a view-based activity
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.

Official references