No history yet

Modern App Lifecycle

Composition and the Lifecycle

In the world of traditional Android views, you managed the UI by directly responding to lifecycle events like onCreate, onStart, and onStop. With Jetpack Compose, the paradigm shifts. The underlying Activity and Fragment lifecycles still exist, but you interact with them differently. Your UI is now a function of state, not a sequence of manual updates.

The core idea is this: the lifecycle of a composable is tied to its presence in the Composition, not directly to the Activity's lifecycle events.

When a composable function is first called during the initial UI build, it enters the . It remains there as long as it's part of the UI tree. If the state it depends on changes, it recomposes (re-runs) to update the UI. When it's no longer needed—perhaps because you navigated to a different screen—it leaves the Composition. This lifecycle is much more granular and happens far more frequently than Activity lifecycle changes.

But what about tasks that need to be tied to the host's lifecycle, not just the Composition? For example, registering a sensor listener when the screen is visible and unregistering it when it's not. If you did this in the body of a composable, you'd re-register it on every recomposition, causing bugs and memory leaks. This is where lifecycle-aware effects come in.

Effects and Cleanup

To handle resources that need careful setup and teardown, you use DisposableEffect. This effect runs its code block when the composable enters the Composition and executes a cleanup function when it leaves.

@Composable
fun SensorReader(lifecycleOwner: LifecycleOwner) {
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                // Connect to sensor
            }
            else if (event == Lifecycle.Event.ON_STOP) {
                // Disconnect from sensor
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        // Cleanup function
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

The onDispose block is the key. It's guaranteed to run when the composable is removed from the UI tree, making it the perfect place to release resources like listeners, observers, or network connections. This prevents memory leaks by ensuring you clean up after yourself.

Surviving Process Death

Configuration changes, like rotating the screen, are common. Your Activity is destroyed and recreated, but your ViewModel survives, preserving its data. But what about a more severe event? What if the user leaves your app, and the Android system, needing memory, kills your app's process entirely? This is called .

When the user returns, the ViewModel is also gone. All the state held in memory is lost. To build a robust app, you must be able to restore your state even after process death.

This is the difference between a good app and a production-ready app. Your app must gracefully handle being terminated by the system.

In Compose, we have two primary tools for this. For UI-specific state that is simple, we use rememberSaveable. For more complex state or business logic data stored in a ViewModel, we use the .

State HolderSurvives RecompositionSurvives Config ChangeSurvives Process Death
remember
rememberSaveable

rememberSaveable works like remember but automatically saves the state to the instance state bundle. It can save primitive types, Parcelable types, and other simple data structures out of the box.

// This counter value will survive rotation AND process death.
var count by rememberSaveable { mutableStateOf(0) }

For the ViewModel, you use SavedStateHandle to persist data that drives your UI. You access it like a map, and it automatically handles the saving and restoring.

class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    // Get a flow for a query that survives process death.
    // Or set a default value if it doesn't exist.
    val userQuery: StateFlow<String> = savedStateHandle.getStateFlow("query", "")

    fun setQuery(newQuery: String) {
        // Updating the handle saves the state automatically.
        savedStateHandle["query"] = newQuery
    }
}

Now, when you collect this state in your UI, you need to do it in a lifecycle-aware way. Collecting a flow continuously in the background wastes resources. The modern way to do this is with collectAsStateWithLifecycle.

// In your Composable

val query by viewModel.userQuery.collectAsStateWithLifecycle()

TextField(value = query, onValueChange = viewModel::setQuery)

This extension function automatically starts collecting the flow when your UI is on screen (in the STARTED state) and stops when it goes off screen. This is the safest and most efficient way to connect your ViewModel state to your Compose UI.

Lesson image

By combining rememberSaveable for simple UI state, SavedStateHandle for critical ViewModel data, and DisposableEffect for resource management, you can build modern Android apps that are robust, efficient, and provide a seamless user experience, even when the system is working against you.

Quiz Questions 1/6

What is the primary purpose of using DisposableEffect in Jetpack Compose?

Quiz Questions 2/6

What is "process death" in the context of Android, and what is its impact on a ViewModel's state?