State and Jetpack Compose

State in an app is any value that can change over time. This is a very broad definition and encompasses 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. This guide focuses on the connection between state and composables, and on the APIs that Jetpack Compose offers to work with state more easily.

State and composition

Compose is declarative and as such the only way to update it is by calling the same composable with new arguments. These arguments are representations of the UI state. Any time a state is updated a recomposition takes place. As a result, things like TextField don’t automatically update like they do in imperative XML based views. A composable has to explicitly be told the new state in order for it to update accordingly.

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = "Hello!",
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = "",
           onValueChange = { },
           label = { Text("Name") }
       )
   }
}

If you run this, you'll see that nothing happens. That's because the TextField doesn't update itself—it updates when its value parameter changes. This is due to how composition and recomposition work in Compose.

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.

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 remembered value as a parameter for other composables or even as logic in statements to change which composables are displayed. For example, if you don't want to display the greeting if the name is empty, use the state in an if statement:

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable. rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.

Other supported types of state

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.

Stateful versus stateless

A composable that uses remember to store an object creates internal state, making the composable stateful. HelloContent is an example of a stateful composable because it holds and modifies its name state internally. This can be useful in situations where a caller doesn't need to control the state and can use it without having to manage the state themselves. However, compasables with internal state tend to be less reusable and harder to test.

A stateless composable is a composable that doesn't hold any state. An easy way to achieve stateless is by using state hoisting.

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.

State hoisting

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. This helps avoid bugs.
  • Encapsulated: Only stateful compoables will be able to modify their state. It's completely internal.
  • Shareable: Hoisted state can be shared with multiple composables. Say we wanted to name in a different composable, hoisting would allow us to do that.
  • Interceptable: callers to the stateless composables 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 name into a ViewModel.

In the example case, you extract the name and the onValueChange out of HelloContent and move them up the tree to a HelloScreen composable that calls HelloContent.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

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

The pattern where the state goes down, and events go up is called a unidirectional data flow. In this case, the state goes down from HelloScreen to HelloContent and events go up from HelloContent to HelloScreen. 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.

ViewModel and state

ViewModels are the recommended state holder for composables that are high up in the Compose UI tree or composables that are destinations in the Navigation library. ViewModels survive configuration changes, so they allow you to encapsulate state and events related to the UI without having to deal with the activity or fragment lifecycle that hosts your Compose code.

Your ViewModels should expose the state in an observable holder, such as LiveData or StateFlow. When the state object is read during a composition, the current recompose scope of the composition is automatically subscribed to updates of that state object.

You can have one or more observable state holders—each of them should hold the state for parts of your screen that are conceptually related and that change together. That way, you preserve a single source of truth, even if the state is used in multiple composables.

You can use LiveData and ViewModel in Jetpack Compose to implement unidirectional data flow. The HelloScreen example would be implemented using a ViewModel like this:

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

    // onNameChange is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChange(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("")
    HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(it) })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

observeAsState observes a LiveData<T> and returns a State<T> object that is updated whenever the LiveData changes. State<T> is an observable type that Jetpack Compose can use directly. observeAsState will observe the LiveData only while it is in the composition.

The line:

val name: String by helloViewModel.name.observeAsState("")

...is syntactic sugar to automatically unwrap the state object that is returned by observeAsState. You can also assign the state object using an assignment operator (=), which makes it a State<String> instead of a String:

val nameState: State<String> = helloViewModel.name.observeAsState("")

Restoring state in Compose

Use rememberSaveable to restore your UI state after an activity or process is recreated. rememberSaveable retains state across recompositions. In addition, rememberSaveable also retains state across activity and process recreation.

Ways to store state

All data types that are added to the Bundle are saved automatically. If you want to save something that cannot be added to the Bundle, there are several options.

Parcelize

The simplest solution is to add the @Parcelize annotation to the object. The object becomes parcelable, and can be bundled. For example, this code makes a parcelable City data type and saves it to the state.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
  var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) }
}

MapSaver

If for some reason @Parcelize is not suitable, you can use mapSaver to define your own rule for converting an object into a set of values that the system can save to the Bundle.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

To avoid needing to define the keys for the map, you can also use listSaver and use its indices as keys:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Learn more

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