Skip to content

Jetpack Compose Concepts

Where do you store state?

There are a few things you need to consider when deciding where to define your state:

  • Where does the data come from?
  • Does it need to persist across runs of the application?
  • Does it need to persist if the device configuration changes (such as the user changing its orientation)?
  • Is the data only used inside a composable function (perhaps it's a scrollbar position that's not needed outside)?

State defined outside of composable functions

Let's start by looking at a very simple Activity that displays a person's name. An Activity manages the user interface for your application. Whatever you call in setContent will declare your Compose UI. setContent creates the root Compose context; composable functions can only be called from it or from other composable functions.

class MainActivity : ComponentActivity() {
    var person by mutableStateOf(Person("Scott"))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text(person.name)
        }
    }
}

Here we define person inside the Activity class, but outside of setContent; it's not inside a composable function.

This works until the device configuration changes. A "configuration change" occurs when, among many other things, the user changes settings (such as language) on the device, folds/unfolds the screen of a foldable device, connects a device to Android Auto, or simply rotates the device from portrait to landscape.

The most common configuration change is device rotation (when "auto rotate" is set), so we'll use that as an example.

When an Activity is created, it uses the device configuration to resolve any resources such as strings and images (and if you're using the old "views" based UIs, layouts, menus, themes and so on). To make this easy on the developer, by default, Android will destroy and recreate the Activity instance so the developer doesn't need to worry about potentially stale resources.

The problem is, when the Activity is destroyed and recreated, any properties it defines are also destroyed and recreated.

This includes person in the above example. If person had been changed, and the user rotates the device, poof! person is reset to Person("Scott").

To deal with this, we could persist the person in a database, but if the data doesn't need to be retained across runs of the application, this is overkill.

ViewModels

This is where ViewModels come in. A ViewModel, in Android terms, is a class that prepares/fetches/holds data for an Activity as long as that Activity is being used. This includes across configuration changes, so it's a great place to store state while the application is running.

We can create a view model like

class MyViewModel: ViewModel() {
    var person by mutableStateOf(Person("Scott"))
}

Note

When adding by mutableStateOf(...) to a file for the first time, you'll see some error underlines. If you float over the error, you see a message indicating that MutableState has no method getValue(...) and it can't be used as a property delegate. The basic MutableState type itself isn't directly usable as a property delegate. We can import

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

which are extension functions that adapt MutableState for use as a property delegate.

You can import these by hitting alt-enter when your cursor is on the red squiggly underline, or clicking the lightbulb to the left of the line.

Back in our Activity, we can use the

class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<MyViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PersonScreen(viewModel.person)
        }
    }
}

The property delegate created by viewModels<MyViewModel>() will create a new instance of MyViewModel when it doesn't yet exist for use with MainActivity, and reuse an existing MyViewModel if it had already been created for MainActivity. Note that if you use viewModels<MyViewModel> in an different Activity class you'll get a different instance. That shouldn't be an issue, as you should usually only be creating a single Activity for your application.

Now if we changed person and rotated the screen, the same person value will be available in the new instance of MainActivity.

Note

While you can create a MutableState in your view model, many don't feel comfortable with bringing a Compose detail into a view model. If you were thinking of sharing your view model across different types of UIs, such as a web UI, command line, or desktop in addition to Compose, you might want to consider using a Kotlin Flow to store the data and collect it inside the UI. We'll use this Flow approach in our examples once we start working with database data. For now, we'll use MutableState.

For the record, I'm on the fence here. If you're only using a Compose UI, directly using MutableState inside your view model is simpler all around. You'll likely end up with a mix of Flow and MutableState in your view model, though, as the data frameworks you likely use (like Room) can produce Flows but not MutableState. The other side of my fence is that desire to keep the view model "clean" of UI specifics.

However - state is part of Compose runtime, which is separate from Compose UI. Unfortunately at the moment there are some inter-dependencies between the two. I hope that someday the runtime can be used separately from the ui, and then we could use runtime as an independent state manager with cool tree-building support.

State inside composable functions

Sometimes the right place to store data is inside a composable function. For example, you can define state that controls the scroll position of a list of items, and may not need it outside of that composable function.

@Composable
fun LongScreen(...) {
    Column(
        modifier = Modifier.verticalScroll(ScrollState(0))
    ) { ... }
}

In this definition, we're defining an instance of ScrollState and passing it into a modifier for Column that controls scrolling.

Unfortunately this won't work - every time we recompose LongScreen, we create a new instance of ScrollState at position 0; the page will never appear to scroll.

We need to create an instance of the ScrollState and reuse it across recompositions. We do this using remember:

@Composable
fun LongScreen(...) {
    val scrollState = remember {
        ScrollState(initial = 0)
    }

    Column(
        modifier = Modifier.verticalScroll(scrollState)
    ) { ... }
}

remember creates a spot in the subtree being declared by a composable function. Its lambda defines the value to store inside that spot. The lambda is only run on initial composition (but we can specify key data that forces it to reevaluate - more on that later).

In this case, we carve out a spot to store a ScrollState inside the tree being declared. The value in that spot will be reused in recomposition, until this subtree no longer exists in the composition. (For example, if you switch to show a different screen in your application).

This works great until a configuration change occurs. Because the Activity is destroyed and recreated, so is the UI. The existing tree is disposed and a new tree is created. This is one of the reasons it's so important that composable functions are idempotent - a tree representing the same data can be recreated.

If we want the data to survive a configuration change, we can use rememberSaveable:

val scrollstate = rememberSaveable(saver = ScrollState.Saver) {
    ScrollState(initial = 0)
}

Note that rememberSaveable requires a saver to be passed in - this describes how the data is saved across configuration changes. In this case, we have

val Saver: Saver<ScrollState, *> = Saver(
    save = { it.value },
    restore = { ScrollState(it) }
)

This saver just returns the value of the ScrollState (its position) to be stored and creates a new ScrollState when being restored.

State objects like ScrollState will often provide a shorthand for the remember block. In this case, we can use

val scrollState = rememberScrollState()
Column(
    modifier = Modifier.verticalScroll(scrollState)
) { ... }

or, if you don't need to look at/modify the scroll state:

Column(
    modifier = Modifier.verticalScroll(rememberScrollState())
) { ... }

As a more interesting example, we can look at TextField for letting the user edit a value:

@Composable
fun MyTextField(
    value: String,                          // AAA
    onValueChange: (String) -> Unit,        // BBB
) {
    var displayedText by remember(value) {  // CCC
        mutableStateOf(value)               // DDD
    }
    TextField(
        value = displayedText,              // EEE
        onValueChange = {
            displayedText = it              // FFF
            onValueChange(it)               // GGG
        }
    )
}

The basic operation of MyTextField is to pass in a value (line AAA) to edit and an event function onValueChange (line BBB) that informs the caller when the value has changed. In an ideal world, as the user types to change the field, the new value is sent to the caller who updates the value, triggering recomposition. The user would see the new value.

Unfortunately, we have no idea what the caller will be doing with the value and how long it will take to come back. Meanwhile the user keeps typing and more onValueChange calls are made. This can result in race conditions and some "interesting" looking text if the user types quickly.

To respond as quickly as possible for the user while they're typing, we create an internal bucket to hold the displayed value:

var displayedText by remember(value) {  // CCC
    mutableStateOf(value)               // DDD
}

On line CCC, we carve out a spot in the tree being declared by MyTextField to hold our bucket. This spot is initialized via its lambda, creating the bucket on line DDD. The value in remember(value) is a key. Whenever that value changes, the lambda is re-executed, creating a new bucket with the new value from the outside.

On line EEE, we pass the value from the bucket to be displayed in the TextField, and whenever the user makes a change, line FFF updates the bucket value, triggering recomposition of the TextField.

Line GGG informs the caller of the change. When we implement this later, we'll debounce the update to limit how often we update the caller.

In this example we're using remember and not rememberSaveable. The source of truth for the value is coming from the outside, and will be passed in again after a configuration change, and the remember will recreate the bucket based on that data.

Note

remember is a composable function, but it returns a value! Any composable function that returns a value must not call composable functions that emit nodes to the tree! Any composable function that emits nodes to the tree must not return a value. Compose makes these assumptions and can perform some optimizations that won't work properly if you don't adhere to these conventions.