Google is committed to advancing racial equity for Black communities. See how.

State and Jetpack Compose

State in an app is any value that can change over time. This is a very broad definition and encompases everything from a Room database to a variable on a class.

All Android apps display state to the user. A few examples of state in Android apps:

  • A Snackbar that shows when a network connection can't be established.
  • A blog post and associated comments.
  • Ripple animations on buttons that play when a user clicks them.
  • Stickers that a user can draw on top of an image.

Jetpack Compose helps you be explicit about where and how you store and use state in an Android app.

UI update loop and events

In an Android app, state is updated in response to events. Events are inputs generated from outside our app, such as the user tapping on a button calling an OnClickListener, an EditText calling afterTextChanged, or an accelerometer sending a new value.

All Android apps have a core UI update loop that looks like this:

The core UI update loop for Android apps.

  • Event: An event is generated by the user or another part of the program.
  • Update state: An event handler changes state.
  • Display state: The UI is updated to display the new state.

In Jetpack Compose, state and events are separate. A state represents a changeable value, while an event represents notification that something happened.

By separating state from events, it is possible to decouple displaying state from the way that state is stored and changed.

Unidirectional data flow in Jetpack Compose

Compose is built for unidirectional data flow. This is a design where state flows down and events flow up.

Figure 1. Unidirectional data flow

By following unidirectional data flow, you can decouple composables that display state in the UI from the parts of your app that store and change state.

The UI update loop for an app using unidirectional data flow looks like this:

  • Event: An event is generated by part of the UI and passed up.
  • Update state: An event handler may change the state.
  • Display state: The state is passed down, and the UI observes the new state and displays it.

Following this pattern when using Jetpack Compose provides several advantages:

  • Testability: by decoupling state from the UI that displays it, it's easier to test both in isolation.
  • State encapsulation: because state can only be updated in one place, it's less likely that you'll create inconsistent states (or bugs).
  • UI consistency: all state updates are immediately reflected in the UI by the use of observable state holders.

ViewModel and unidirectional data flow

When you use ViewModel and LiveData from Android Architecture Components, you introduce unidirectional data flow in your app.

Before looking at ViewModels with Compose, consider an Activity using Android Views and unidirectional data flow that displays "Hello, ${name}" and allows the user to input their name.

An example of user input with ViewModels.

The code for this screen using a ViewModel and an Activity:

class HelloViewModel : ViewModel() {

    // LiveData holds state which is observed by the UI
    // (state flows down from ViewModel)
    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name

    // onNameChanged is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChanged(newName: String) {
        _name.value = newName
    }
}

class HelloActivity : AppCompatActivity() {
    val helloViewModel by viewModels<HelloViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // binding represents the activity layout, inflated with ViewBinding
        binding.textInput.doAfterTextChanged {
            helloViewModel.onNameChanged(it.toString())
        }

        helloViewModel.name.observe(this) { name ->
            binding.helloText.text = "Hello, $name"
        }
    }
}

By using Android Architecture Components, we've introduced a unidirectional data flow design to this Activity.

Figure 2. Unidirectional data flow in an Activity using ViewModel

To see how unidirectional data flow works in the UI update loop, consider the loop for this Activity:

  1. Event: onNameChanged is called by the UI when the text input changes.
  2. Update state: onNameChanged does processing, then sets the state of _name.
  3. Display state: name's observers are called and the UI displays the new state.

ViewModel and Jetpack Compose

You can use LiveData and ViewModel in Jetpack Compose to implement unidirectional data flow, just like you did in an Activity in the previous section.

Here's code for the same screen as HelloActivity written in Jetpack Compose using the same HelloViewModel:

class HelloViewModel : ViewModel() {

    // LiveData holds state which is observed by the UI
    // (state flows down from ViewModel)
    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name

    // onNameChanged is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChanged(newName: String) {
        _name.value = newName
    }
}

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
    // by default, viewModel() follows the Lifecycle as the Activity or Fragment
    // that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.

    // name is the _current_ value of [helloViewModel.name]
    // with an initial value of ""
    val name: String by helloViewModel.name.observeAsState("")

    Column {
        Text(text = name)
        TextField(
            value = name,
            onValueChange = { helloViewModel.onNameChanged(it) },
            label = { Text("Name") }
        )
    }
}

HelloViewModel and HelloScreen follow the design of unidirectional data flow. State flows down from HelloViewModel, and events flow up from HelloScreen.

The unidirectional flow between the view model and hello screen.

Consider the UI event loop for this composable:

  1. Event: onNameChanged is called in response to the user typing a character.
  2. Update state: onNameChanged does processing, then sets the state of _name.
  3. Display state: name's value changes, which is observed by compose in observeAsState. Then, HelloScreen runs again (or recomposes) to describe the UI based on the new value of name.

To learn more about how to use ViewModel and LiveData to build unidirectional data flow on Android, read the guide to app architecture.

Stateless composables

A stateless composable is a composable that cannot change any state itself. Stateless components are easier to test, tend to have fewer bugs, and open up more opportunities for reuse.

If your composable has state, you can make it stateless by using state hoisting. State hoisting is a programming pattern where you move state to the caller of a composable by replacing internal state in a composable with a parameter and events.

To see an example of state hoisting, extract a stateless composable out of HelloScreen.

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
    // helloViewModel follows the Lifecycle as the Activity or Fragment that calls this
    // composable function. This lifecycle can be modified by callers of HelloScreen.

    // name is the _current_ value of [helloViewModel.name]
    val name: String by helloViewModel.name.observeAsState("")

    HelloInput(name = name, onNameChange = { helloViewModel.onNameChanged(it) })
}

@Composable
fun HelloInput(
    name: String, /* state */
    onNameChange: (String) -> Unit /* event */
) {
    Column {
        Text(name)
        TextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

HelloInput has access to the state as an immutable String parameter, as well as an event onNameChange that it can call when it wants to request the state change.

Lambdas are the most common way to describe events on a composable. Here we're defining an event onNameChange using a lambda that takes a String, using Kotlin's function type syntax (String) -> Unit. Note that onNameChange is present tense, as the event doesn't mean the state has already changed, but that the composable is requesting that the event handler change it.

HelloScreen is a stateful composable because it has a dependency on the final class, HelloViewModel, which can directly change the name state. There's no way for the caller of HelloScreen to control updates to the name state. HelloInput is a stateless composable because it doesn't have the ability to directly change any state.

By hoisting the state out of HelloInput, it's easier to reason about the composable, reuse it in different situations, and test. HelloInput is decoupled from how the state is stored. Decoupling means if you modify or replace HelloViewModel, you don't have to change how HelloInput is implemented.

The process of state hoisting allows you to extend unidirectional dataflow to stateless composables. The unidirectional data flow diagram for these composables maintains state going down, and events going up as more composables interact with the state.

The state and event flow between HelloInput, HelloScreen, and HelloViewModel

It's important to understand that a stateless composable can still interact with state that changes over time by using unidirectional data flow and state hoisting.

To understand how this works, consider the UI Update Loop for HelloInput:

  1. Event: onNameChange is called in response to the user typing a character.
  2. Update state: HelloInput can't directly modify state. The caller may choose to modify state(s) in response to the onNameChange event. Here the caller, HelloScreen, will call onNameChanged on HelloViewModel which causes the name state to update.
  3. Display state: When name's value changes, HelloScreen is called again with the updated name due to observeAsState. It will in turn call HelloInput again with the new name parameter. Calling composables again in response to state changes is called recomposition.

Composition and recomposition

A Composition describes the UI and is produced by running composables. A Composition is a tree-structure of the composables that describe your UI.

During initial composition, Jetpack Compose will keep track of the composables that you call to describe your UI in a Composition. Then, when the state of your app changes, Jetpack Compose schedules recomposition. Recomposition is running the composables that may have changed in response to state changes, and Jetpack Compose updates the Composition to reflect any changes.

A Composition can only be produced by an initial composition and updated by recomposition. The only way to modify a Composition is through recomposition.

To learn more about initial composition and recomposition, see Thinking in Compose.

State in composables

Composable functions can store a single object in memory by using the remember composable. A value computed by remember is stored in the Composition during initial composition, and the stored value is returned during recomposition. remember can be used to store both mutable and immutable objects.

Use remember to store immutable values

You can store immutable values when caching expensive UI operations, such as computing text formatting. The remembered value is stored in the Composition with the composable that called remember.

@Composable
fun FancyText(text: String) {
    // by passing text as a parameter to remember, it will re-run the calculation on
    // recomposition if text has changed since the last recomposition
    val formattedText = remember(text) { computeTextFormatting(text) }
    /*...*/
}
Figure 3. Composition of FancyText with formattedText as child

Use remember to create internal state in a composable

When you store a mutable object using remember, you add state to a composable. You can use this approach to create internal state for a single stateful composable.

We strongly recommend that all mutable state used by composables be observable. This allows compose to automatically recompose whenever the state changes. Compose comes with a built-in observable type State<T> which is directly integrated into the Compose runtime.

A good example of internal state in a composable is an ExpandingCard that animates between collapsed and expanded when the user clicks a button.

Figure 4. ExpandedCard composable animates between collapsed and expanded

This composable has one important state: expanded. When expanded the composable should show the body, and when collapsed it should hide the body.

Figure 5. Composition of ExpandingCard with expanded state as child

You can add a state expanded to a composable by remembering mutableStateOf(initialValue).

@Composable
fun ExpandingCard(title: String, body: String) {
    // expanded is "internal state" for ExpandingCard
    var expanded by remember { mutableStateOf(false) }

    // describe the card for the current state of expanded
    Card {
        Column(
            Modifier
                .width(280.dp)
                .animateContentSize() // automatically animate size when it changes
                .padding(top = 16.dp, start = 16.dp, end = 16.dp)
        ) {
            Text(text = title)

            // content of the card depends on the current value of expanded
            if (expanded) {
                // TODO: show body & collapse icon
            } else {
                // TODO: show expand icon
            }
        }
    }
}

mutableStateOf creates an observable MutableState<T>, which is an observable type integrated with the compose runtime.

interface MutableState<T> : State<T> {
   override var value: T
}

Any changes to value will schedule recomposition of any composable functions that read value. In the case of ExpandingCard, whenever expanded changes, it causes ExpandingCard to be recomposed.

There are three ways to declare a MutableState object in a composable:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

These declarations are equivalent, and are provided as syntax sugar for different uses of state. You should pick the one that produces the easiest-to-read code in the composable you're writing.

You can use the value of internal state in a composable as a parameter to another composable, or even to change what composables are called. In ExpandingCard an if-statement will change the content of the card based on the current value of expanded.

if (expanded) {
   // TODO: show body & collapse icon
} else {
   // TODO: show expand icon
}

Modify internal state in a composable

State should be modified by events in a composable. If you modify state when running a composable instead of in an event, this is a side-effect of the composable, which should be avoided. For more information about side-effects in Jetpack Compose, see Thinking in Compose.

To complete the ExpandingCard composable, let’s display the body and a collapse button when expanded is true and an expand button when expanded is false.

@Composable
fun ExpandingCard(title: String, body: String) {
    var expanded by remember { mutableStateOf(false) }

    // describe the card for the current state of expanded
    Card {
        Column(
            Modifier
                .width(280.dp)
                .animateContentSize() // automatically animate size when it changes
                .padding(top = 16.dp, start = 16.dp, end = 16.dp)
        ) {
            Text(text = title)

            // content of the card depends on the current value of expanded
            if (expanded) {
                Text(text = body, Modifier.padding(top = 8.dp))
                // change expanded in response to click events
                IconButton(onClick = { expanded = false }, modifier = Modifier.fillMaxWidth()) {
                    Icon(Icons.Default.ExpandLess)
                }
            } else {
                // change expanded in response to click events
                IconButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
                    Icon(Icons.Default.ExpandMore)
                }
            }
        }
    }
}

In this composable, the state is modified in response to onClick events. Since expanded is using var with the property delegate syntax, the onClick callbacks can assign expanded directly.

IconButton(onClick = { expanded = true }, /* … */) {
   // ...
}

We can now describe the UI Update Loop for ExpandingCard to see how internal state is modified and used by Compose.

  1. Event: onClick is called in response to the user tapping on one of the buttons.
  2. Update state: expanded is changed in the onClick listener using assignment.
  3. Display state: ExpandingCard recomposes because expanded is State<Boolean> that changed, and ExpandingCard reads it in the line if(expanded). ExpandingCard then describes the screen for the new value of expanded.

Use other types of state in Jetpack Compose

Jetpack Compose doesn't require that you use MutableState<T> to hold state. Jetpack Compose supports other observable types. Before reading another observable type in Jetpack Compose, you must convert it to a State<T> so that Jetpack Compose can automatically recompose when the state changes.

Compose ships with functions to create State<T> from common observable types used in Android apps:

You can build an extension function for Jetpack Compose to read other observable types if your app uses a custom observable class. See the implementation of the builtins for examples of how to do this. Any object that allows Jetpack Compose to subscribe to every change can be converted to State<T> and read by a composable.

You can also build integration layers for non-observable state objects by using invalidate to manually trigger recomposition. This should be reserved for situations where you must interoperate with a non-observable type. Using invalidate is easy to get wrong and tends to lead to complex code that's harder to read than the same code using observable state objects.

Separate internal state from UI composables

The ExpandingCard in the last section has internal state. As a result, the caller cannot control the state. This means, for example, that if you wanted to start an ExpandingCard in the expanded state, there is no way to do so. You also can't make the card expand in response to another event, such as the user clicking on a Fab. It also means that if you wanted to move the expanded state into a ViewModel, you couldn't do it.

On the other hand, by using internal state in ExpandingCard, a caller that doesn't need to control or hoist the state can use it without having to manage the state themselves.

As you develop reusable composables, you often want to expose both a stateful and a stateless version of the same composable. The stateful version is convenient for callers that don't care about the state, and the stateless version is necessary for callers that need to control or hoist the state.

To provide both as a stateful and stateless interfaces, extract a stateless composable that displays the UI using state hoisting.

Notice that both composables are both named ExpandingCard even though they take different parameters. The naming convention for composables that emit UI is a CapitalCase noun that describes what the composable represents on the screen. In this case, they both represent an ExpandingCard. This naming convention applied throughout the Compose libraries, for example in TextField and TextField.

Here's ExpandingCard split into a stateful and stateless composables:

// this stateful composable is only responsible for holding internal state
// and defers the UI to the stateless composable
@Composable
fun ExpandingCard(title: String, body: String) {
    var expanded by remember { mutableStateOf(false) }
    ExpandingCard(
        title = title,
        body = body,
        expanded = expanded,
        onExpand = { expanded = true },
        onCollapse = { expanded = false }
    )
}

// this stateless composable is responsible for describing the UI based on the state
// passed to it and firing events in response to the buttons being pressed
@Composable
fun ExpandingCard(
    title: String,
    body: String,
    expanded: Boolean,
    onExpand: () -> Unit,
    onCollapse: () -> Unit
) {
    Card {
        Column(
            Modifier
                .width(280.dp)
                .animateContentSize() // automatically animate size when it changes
                .padding(top = 16.dp, start = 16.dp, end = 16.dp)
        ) {
            Text(title)
            if (expanded) {
                Spacer(Modifier.height(8.dp))
                Text(body)
                IconButton(onClick = onCollapse, Modifier.fillMaxWidth()) {
                    Icon(Icons.Default.ExpandLess)
                }
            } else {
                IconButton(onClick = onExpand, Modifier.fillMaxWidth()) {
                    Icon(Icons.Default.ExpandMore)
                }
            }
        }
    }
}

State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:

  • value: T: the current value to display
  • onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value

However, you are not limited to onValueChange. If more specific events are appropriate for the composable you should define them using lambdas like ExpandingCard does with onExpand and onCollapse.

State that is hoisted this way has some important properties:

  • Single source of truth: by moving state instead of duplicating it, we're ensuring there's only one source of truth for expanded. This helps avoid bugs.
  • Encapsulated: only stateful ExpandingCard will be able to modify its state. It's completely internal.
  • Shareable: hoisted state can be shared with multiple composables. Say we wanted to hide a Fab button when the Card is expanded, hoisting would allow us to do that.
  • Interceptable: callers to the stateless ExpandingCard can decide to ignore or modify events before changing the state.
  • Decoupled: the state for the stateless ExpandingCard may be stored anywhere. For example, it's now possible to move title, body, and expanded into a ViewModel.

Hosting this way also follows unidirectional data flow. The state is passed down from the stateful composable, and events flow up from the stateless composable.

Figure 6. Unidirectional data flow diagram for stateful and stateless ExpandingCard

Internal state and configuration changes

Values that are remembered by remember in a Composition are forgotten and recreated during configuration changes such as rotation.

If you use remember { mutableStateOf(false) }, the stateful ExpandingCard resets to collapsed whenever the user rotates the phone. We can fix that by using saved instance state instead, to automatically save and restore the state on configuration changes.

@Composable
fun ExpandingCard(title: String, body: String) {
    var expanded by savedInstanceState { false }
    ExpandingCard(
        title = title,
        body = body,
        expanded = expanded,
        onExpand = { expanded = true },
        onCollapse = { expanded = false }
    )
}

The composable function savedInstanceState<T> returns a MutableState<T> that automatically saves and restores itself on configuration changes. You should use it for any internal state that a user would expect to survive configuration changes.

Learn more

To learn more about state and Jetpack Compose, take the Using State in Jetpack Compose.