State in Jetpack Compose

1. Before you begin

This codelab explains the core concepts related to using State in Jetpack Compose. It shows you how the app's state determines what is displayed in the UI, how Compose updates the UI when state changes by working with different APIs, how to optimize the structure of our composable functions, and using ViewModels in a Compose world.

Prerequisites

  • Knowledge of Kotlin syntax.
  • Basic understanding of Compose (you can start with the Jetpack Compose tutorial).
  • Basic understanding of Architecture Component's ViewModel.

What you'll learn

  • How to think about state and events in a Jetpack Compose UI.
  • How Compose uses state to determine which elements to display on the screen.
  • What state hoisting is.
  • How stateful and stateless composable functions work.
  • How Compose automatically tracks state with the State<T> API.
  • How memory and internal state work in a composable function: using the remember and rememberSaveable APIs.
  • How to work with lists and state: using the mutableStateListOf and toMutableStateList APIs.
  • How to use ViewModel with Compose.

What you'll need

Recommended/Optional

What you'll build

You will implement a simple Wellness app:

775940a48311302b.png

The app has two main functionalities:

  • A water counter to track your water intake.
  • A list of wellness tasks to do throughout the day.

For more support as you're walking through this codelab, check out the following code-along:

2. Get set up

Start a new Compose project

  1. To start a new Compose project, open Android Studio.
  2. If you're in the Welcome to Android Studio window, click Start a new Android Studio project. If you already have an Android Studio project open, select File > New > New Project from the menu bar.
  3. For a new project, choose Empty Activity from the available templates.

New project

  1. Click Next and configure your project, calling it "BasicStateCodelab".

Make sure you select a minimumSdkVersion of at least API level 21, which is the minimum API Compose supports.

When you choose the Empty Compose Activity template, Android Studio sets up the following for you in your project:

  • A MainActivity class configured with a composable function that displays some text on the screen.
  • The AndroidManifest.xml file, which defines your app's permissions, components, and custom resources.
  • The build.gradle.kts and app/build.gradle.kts files contain options and dependencies needed for Compose.

Solution to the codelab

You can get the solution code for the BasicStateCodelab from GitHub:

$ git clone https://github.com/android/codelab-android-compose

Alternatively you can download the repository as a Zip file.

You'll find the solution code in the BasicStateCodelab project. We recommend that you follow the codelab step by step at your own pace and check the solution if you need help. During the codelab, you are presented with snippets of code that you need to add to your project.

3. State in Compose

An app's "state" is any value that can change over time. This is a very broad definition and encompasses everything from a Room database to a variable in a class.

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

  • The most recent messages received in a chat app.
  • The user's profile photo.
  • The scroll position in a list of items.

Let's start writing your Wellness app.

For simplicity, during the codelab:

  • You can add all Kotlin files in the root com.codelabs.basicstatecodelab package of the app module. In a production app, however, files should be logically structured in subpackages.
  • You'll hardcode all strings inline in snippets. In a real app, they should be added as string resources in the strings.xml file and referenced using Compose's stringResource API.

The first piece of functionality you need to build is a water counter to count the number of glasses of water you consume during the day.

Create a composable function called WaterCounter that contains a Text composable that displays the number of glasses. The number of glasses should be stored in a value called count, which you can hardcode for now.

Create a new file WaterCounter.kt with the WaterCounter composable function, like this:

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   val count = 0
   Text(
       text = "You've had $count glasses.", 
       modifier = modifier.padding(16.dp)
   )
}

Let's create a composable function that represents the whole screen, which will have two sections, the water counter and the list of wellness tasks. For now we'll just add our counter.

  1. Create a file WellnessScreen.kt, which represents the main screen, and call our WaterCounter function:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. Open the MainActivity.kt. Remove the Greeting and the DefaultPreview composables. Call the newly created WellnessScreen composable inside the Activity's setContent block, like this:
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           BasicStateCodelabTheme {
               // A surface container using the 'background' color from the theme
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colorScheme.background
               ) {
                   WellnessScreen()
               }
           }
       }
   }
}
  1. If you run the app now, you'll see our basic water counter screen with the hardcoded count of glasses of water.

7ed1e6fbd94bff04.jpeg

The state of the WaterCounter composable function is the variable count. But having a static state is not very useful as it cannot be modified. To remedy this, you'll add a Button to increase the count and track the amount of glasses of water you have throughout the day.

Any action that causes the modification of state is called an "event" and we'll learn more about this in the next section.

4. Events in Compose

We talked about state as any value that changes over time, for example, the last messages received in a chat app. But what causes the state to update? In Android apps, state is updated in response to events.

Events are inputs generated from outside or inside an application, such as:

  • The user interacting with the UI by, for example, pressing a button.
  • Other factors, such as sensors sending a new value, or network responses.

While the state of the app offers a description of what to display in the UI, events are the mechanism through which the state changes, resulting in changes to the UI.

Events notify a part of a program that something has happened. In all Android apps, there's a core UI update loop that goes like this:

f415ca9336d83142.png

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

Managing state in Compose is all about understanding how state and events interact with each other.

Now, add the button so that users can modify the state by adding more glasses of water.

Go to the WaterCounter composable function to add the Button below our label Text. A Column will help you vertically align the Text with the Button composables. You can move the external padding to the Column composable and add some extra padding to the top of the Button so it's separated from the Text.

The Button composable function receives an onClick lambda function - this is the event that happens when the button is clicked. You'll see more examples of lambda functions later.

Change count to var instead of val so it becomes mutable.

import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count = 0
       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

When you run the app and click the button, notice that nothing happens. Setting a different value for the count variable won't make Compose detect it as a state change so nothing happens. This is because you haven't told Compose that it should redraw the screen (that is, "recompose" the composable function), when the state changes. You'll fix this in the next step.

e4dfc3bef967e0a1.gif

5. Memory in a composable function

Compose apps transform data into UI by calling composable functions. We refer to the Composition as the description of the UI built by Compose when it executes composables. If a state change happens, Compose re-executes the affected composable functions with the new state, creating an updated UI—this is called recomposition. Compose also looks at what data an individual composable needs, so that it only recomposes components whose data has changed and skips those that are not affected.

To be able to do this, Compose needs to know what state to track, so that when it receives an update it can schedule the recomposition.

Compose has a special state tracking system in place that schedules recompositions for any composables that read a particular state. This lets Compose be granular and just recompose those composable functions that need to change, not the whole UI. This is done by tracking not only "writes" (that is, state changes), but also "reads" to the state.

Use Compose's State and MutableState types to make state observable by Compose.

Compose keeps track of each composable that reads State value properties and triggers a recomposition when its value changes. You can use the mutableStateOf function to create an observable MutableState. It receives an initial value as a parameter that is wrapped in a State object, which then makes its value observable.

Update WaterCounter composable, so that count uses mutableStateOf API with 0 as initial value. As mutableStateOf returns a MutableState type, you can update its value to update the state, and Compose will trigger a recomposition to those functions where its value is read.

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       // Changes to count are now tracked by Compose
       val count: MutableState<Int> = mutableStateOf(0)

       Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

As mentioned earlier, any changes to count schedules a recomposition of any composable functions that read count's value automatically. In this case, WaterCounter is recomposed whenever the button is clicked.

If you run the app now, you'll notice again that nothing happens yet!

e4dfc3bef967e0a1.gif

Scheduling recompositions is working fine. However, when a recomposition happens, the variable count is re-initialized back to 0, so we need a way to preserve this value across recompositions.

For this we can use the remember composable inline function. A value calculated by remember is stored in the Composition during the initial composition, and the stored value is kept across recompositions.

Usually remember and mutableStateOf are used together in composable functions.

There are a few equivalent ways to write this as shown in the Compose State documentation.

Modify WaterCounter, surrounding the call to mutableStateOf with the remember inline composable function:

import androidx.compose.runtime.remember

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = remember { mutableStateOf(0) } 
        Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

Alternatively, we could simplify the usage of count by using Kotlin's delegated properties.

You can use the by keyword to define count as a var. Adding the delegate's getter and setter imports lets us read and mutate count indirectly without explicitly referring to the MutableState's value property every time.

Now WaterCounter looks like this:

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

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

You should pick the syntax that produces the easiest-to-read code in the composable you're writing.

Now let's examine what we've done so far:

  • Defined a variable that we remember over time called count.
  • Created a text display where we tell the user the number we remembered.
  • Added a button that increments the number we remembered whenever it's clicked.

This arrangement forms a data flow feedback loop with the user:

  • The UI presents the state to the user (the current count is displayed as text).
  • The user produces events that are combined with existing state to produce new state (clicking the button adds one to the current count)

Your counter is ready and working!

a9d78ead2c8362b6.gif

6. State driven UI

Compose is a declarative UI framework. Instead of removing UI components or changing their visibility when state changes, we describe how the UI is under specific conditions of state. As a result of a recomposition being called and UI updated, composables might end up entering or leaving the Composition.

7d3509d136280b6c.png

This approach avoids the complexity of manually updating views as you would with the View system. It's also less error-prone, as you can't forget to update a view based on a new state, because it happens automatically.

If a composable function is called during the initial composition or in recompositions, we say it is present in the Composition. A composable function that is not called—for example, because the function is called inside an if statement and the condition is not met—-is absent from the Composition.

You can learn more about the lifecycle of composables in the documentation.

The output of the Composition is a tree-structure that describes the UI.

You can inspect the app layout generated by Compose using Android Studio's Layout inspector tool, which is what you'll do next.

To demonstrate this, modify your code to show UI based on state. Open WaterCounter and show the Text if the count is greater than 0:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       if (count > 0) {
           // This text is present if the button has been clicked
           // at least once; absent otherwise
           Text("You've had $count glasses.")
       }
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Run the app, and open Android Studio's Layout inspector tool by navigating to Tools > Layout Inspector.

You'll see a split screen: the components tree to the left and a preview of the app to the right.

Navigate the tree by tapping the root element BasicStateCodelabTheme on the left of the screen. Expand the whole component tree by clicking the Expand all button.

Clicking on an element in the screen on the right navigates to the corresponding element of the tree.

677bc0a178670de8.png

If you press the Add one button on the app:

  • Count increases to 1 and the state changes.
  • A recomposition is called.
  • Screen gets recomposed with the new elements.

When you examine the component tree with Android Studio's Layout inspector tool, now you see the Text composable as well:

1f8e05f6497ec35f.png

State drives which elements are present in the UI at a given moment.

Different parts of the UI can depend on the same state. Modify the Button so it's enabled until count is 10 and is then disabled (and you reach your goal for the day). Use the Button's enabled parameter to do this.

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    ...
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
    ...
}

Run the app now. Changes to state count determine whether or not to show the Text, and whether the Button is enabled or disabled.

1a8f4095e384ba01.gif

7. Remember in Composition

remember stores objects in the Composition, and forgets the object if the source location where remember is called is not invoked again during a recomposition.

To visualize this behavior, you'll implement the following piece of functionality in the app: when the user has had at least one glass of water, display a wellness task for the user to do, that they can also close. Because composables should be small and reusable, create a new composable called WellnessTaskItem that displays the wellness task based on a string received as a parameter, along with a Close icon button.

Create a new file WellnessTaskItem.kt, and add the following code. You'll use this composable function later in the codelab.

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

@Composable
fun WellnessTaskItem(
    taskName: String,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier.weight(1f).padding(start = 16.dp),
            text = taskName
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}

The WellnessTaskItem function receives a task description and an onClose lambda function (just like the built-in Button composable receives an onClick).

WellnessTaskItem looks like this:

6e8b72a529e8dedd.png

To improve our app with more features, update WaterCounter to show the WellnessTaskItem when count > 0.

When count is greater than 0, define a variable showTask that determines whether or not to show the WellnessTaskItem and initialize it to true.

Add a new if statement to show WellnessTaskItem if showTask is true. Use the APIs you learned in the previous sections to make sure showTask value survives recompositions.

@Composable
fun WaterCounter() {
   Column(modifier = Modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { },
                   taskName = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Button(onClick = { count++ }, enabled = count < 10) {
           Text("Add one")
       }
   }
}

Use the WellnessTaskItem's onClose lambda function, so that when the X button is pressed, the variable showTask changes to false and the task isn't shown anymore.

   ...
   WellnessTaskItem(
      onClose = { showTask = false },
      taskName = "Have you taken your 15 minute walk today?"
   )
   ...

Next, add a new Button with the text "Clear water count" and place it beside the "Add one" Button. A Row can help align the two buttons. You can also add some padding to the Row. When the "Clear water count" button is pressed, the variable count resets back to 0.

Your WaterCounter composable function should look like this:

import androidx.compose.foundation.layout.Row


@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { showTask = false },
                   taskName = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Row(Modifier.padding(top = 8.dp)) {
           Button(onClick = { count++ }, enabled = count < 10) {
               Text("Add one")
           }
           Button(
               onClick = { count = 0 }, 
               Modifier.padding(start = 8.dp)) {
                   Text("Clear water count")
           }
       }
   }
}

When you run the app, your screen shows the initial state:

Tree of components diagram, showing the app's initial state, count is 0

To the right, we have a simplified version of the components tree, which will help you analyze what is happening as state changes. count and showTask are remembered values.

Now you can follow these steps in the app:

  • Press the Add one button. That increments count (this causes a recomposition) and both WellnessTaskItem and counter Text start to display.

Tree of components diagram, showing state change, when Add one button is clicked, Text with tip appears and Text with glasses count appears.

865af0485f205c28.png

  • Press the X of WellnessTaskItem component (this causes another recomposition). showTask is now false, which means WellnessTaskItem isn't displayed anymore.

Tree of components diagram, showing that when close button is clicked, the task composable disappears.

82b5dadce9cca927.png

  • Press the Add one button (another recomposition). showTask remembers you've closed WellnessTaskItem in the next recompositions if you keep adding glasses.

  • Press the Clear water count button to reset count to 0 and cause a recomposition. Text showing count, and all code related to WellnessTaskItem, are not invoked and leave the Composition.

ae993e6ddc0d654a.png

  • showTask is forgotten because the code location where remember showTask is called was not invoked. You're back to the first step.

  • Press the Add one button making count greater than 0 (recomposition).

7624eed0848a145c.png

  • WellnessTaskItem composable displays again, because the previous value of showTask was forgotten when it left the Composition above.

What if we require showTask to persist after count goes back to 0, longer than what remember allows (that is, even if the code location where remember is called is not invoked during a recomposition)? We'll explore how to fix these scenarios and more examples in the next sections.

Now that you understand how the UI and state are reset when they leave the Composition, clear your code and go back to the WaterCounter you had at the beginning of this section:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        if (count > 0) {
            Text("You've had $count glasses.")
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
            Text("Add one")
        }
    }
}

8. Restore state in Compose

Run the app, add some glasses of water to the counter, and then rotate your device. Make sure you have the device's Auto-rotate setting on.

Because Activity is recreated after a configuration change (in this case, orientation), the state that was saved is forgotten: the counter disappears as it goes back to 0.

2c1134ad78e4b68a.gif

The same happens if you change language, switch between dark and light mode, or any other configuration change that makes Android recreate the running Activity.

While remember helps you retain state across recompositions, it's not retained across configuration changes. For this, you must use rememberSaveable instead of remember.

rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object. For more information on Restoring state in Compose, check out the documentation.

In WaterCounter, replace remember with rememberSaveable:

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
        ...
        var count by rememberSaveable { mutableStateOf(0) }
        ...
}

Run the app now and try some configuration changes. You should see the counter is properly saved.

bf2e1634eff47697.gif

Activity recreation is just one of the use cases of rememberSaveable. We'll explore another use case later while working with lists.

Consider whether to use remember or rememberSaveable depending on your app's state and UX needs.

9. State hoisting

A composable that uses remember to store an object contains internal state, which makes the composable stateful. This is 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, composables with internal state tend to be less reusable and harder to test.

Composables that don't hold any state are called stateless composables. An easy way to create a stateless composable is by using 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 with a new value T

where this value represents any state that could be modified.

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.
  • Shareable: Hoisted state can be shared with multiple composables.
  • Interceptable: Callers to the stateless composables can decide to ignore or modify events before changing the state.
  • Decoupled: The state for a stateless composable function can be stored anywhere. For example, in a ViewModel.

Try to implement this for the WaterCounter so it can benefit from all of the above.

Stateful vs Stateless

When all state can be extracted from a composable function the resulting composable function is called stateless.

Refactor WaterCounter composable by splitting it into two parts: stateful and stateless Counter.

The role of the StatelessCounter is to display the count and call a function when you increment the count. To do this, follow the pattern described above and pass the state, count (as a parameter to the composable function), and a lambda (onIncrement), that is called when the state needs to be incremented. StatelessCounter looks like this:

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count > 0) {
           Text("You've had $count glasses.")
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
           Text("Add one")
       }
   }
}

StatefulCounter owns the state. That means that it holds the count state and modifies it when calling the StatelessCounter function.

@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}

Good job! You hoisted count from StatelessCounter to StatefulCounter.

You can plug this into your app and update WellnessScreen with the StatefulCounter:

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}

As mentioned, state hoisting has some benefits. We'll explore variations of this code to explain some of them, you don't need to copy the following snippets in your app.

  1. Your stateless composable can now be reused. Take for instance the following example.

To count glasses of water and of juice you remember the waterCount and the juiceCount, but use the same StatelessCounter composable function to display two different independent states.

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

8211bd9e0a4c5db2.png

If juiceCount is modified then StatefulCounter is recomposed. During recomposition, Compose identifies which functions read juiceCount and triggers recomposition of only those functions.

2cb0dcdbe75dcfbf.png

When the user taps to increment juiceCount, StatefulCounter recomposes, and so does the StatelessCounter that reads juiceCount. But the StatelessCounter that reads waterCount is not recomposed.

7fe6ee3d2886abd0.png

  1. Your stateful composable function can provide the same state to multiple composable functions.
@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count *= 2 })
}

In this case, if the count is updated by either StatelessCounter or AnotherStatelessMethod, everything is recomposed, which is expected.

Because hoisted state can be shared, be sure to pass only the state that the composables need to avoid unnecessary recompositions, and to increase reusability.

To read more about state and state hoisting, check out the Compose State documentation.

10. Work with lists

Next, add the second feature of your app, the list of wellness tasks. You can perform two actions with items on the list:

  • Check list items to mark the task as completed.
  • Remove tasks from the list you're not interested in completing.

Setup

  1. First, modify the list item. You can reuse the WellnessTaskItem from the Remember in Composition section, and update it to contain the Checkbox. Make sure that you hoist the checked state and the onCheckedChange callback to make the function stateless.

a0f8724cfd33cb10.png

The WellnessTaskItem composable for this section should look like this:

import androidx.compose.material3.Checkbox

@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}
  1. In the same file, add a stateful WellnessTaskItem composable function that defines a state variable checkedState and passes it to the stateless method of the same name. Don't worry about onClose for now, you can pass an empty lambda function.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
   var checkedState by remember { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = {}, // we will implement this later!
       modifier = modifier,
   )
}
  1. Create a file WellnessTask.kt to model a task that contains an ID and a label. Define it as a data class.
data class WellnessTask(val id: Int, val label: String)
  1. For the list of tasks itself, create a new file named WellnessTasksList.kt and add a method that generates some fake data:
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

Note that in a real app, you get your data from your data layer.

  1. In WellnessTasksList.kt, add a composable function that creates the list. Define a LazyColumn and items from the list method you created. Check out the Lists documentation if you need help.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember

@Composable
fun WellnessTasksList(
    modifier: Modifier = Modifier,
    list: List<WellnessTask> = remember { getWellnessTasks() }
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(list) { task ->
            WellnessTaskItem(taskName = task.label)
        }
    }
}
  1. Add the list to WellnessScreen. Use a Column to help vertically align the list with the counter you already have.
import androidx.compose.foundation.layout.Column

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()
       WellnessTasksList()
   }
}
  1. Run the app and give it a try! You should now be able to check tasks but not delete them. You'll implement that in a later section.

f9cbc49c960fd24c.gif

Restore item state in LazyList

Take a closer look now at some things in the WellnessTaskItem composables.

checkedState belongs to each WellnessTaskItem composable independently, like a private variable. When checkedState changes, only that instance of WellnessTaskItem gets recomposed, not all WellnessTaskItem instances in the LazyColumn.

Try it out by following these steps:

  1. Check any element at the top of this list (for example elements 1 and 2).
  2. Scroll to the bottom of the list so that they're off the screen.
  3. Scroll back to the top to the items you checked before.
  4. Notice that they are unchecked.

There is an issue, as you saw in a previous section, that when an item leaves the Composition, state that was remembered is forgotten. For items on a LazyColumn, items leave the Composition entirely when you scroll past them and they're no longer visible.

a68b5473354d92df.gif

How do you fix this? Once again, use rememberSaveable. Your state will survive the activity or process recreation using the saved instance state mechanism. Thanks to how rememberSaveable works together with the LazyList, your items are able to also survive leaving the Composition.

Just replace remember with rememberSaveable in your stateful WellnessTaskItem, and that's it:

import androidx.compose.runtime.saveable.rememberSaveable

var checkedState by rememberSaveable { mutableStateOf(false) }

85796fb49cf5dd16.gif

Common patterns in Compose

Notice the implementation of LazyColumn:

@Composable
fun LazyColumn(
...
    state: LazyListState = rememberLazyListState(),
...

The composable function rememberLazyListState creates an initial state for the list using rememberSaveable. When the Activity is recreated, the scroll state is maintained without you having to code anything.

Many apps need to react and listen to scroll position, item layout changes, and other events related to the list's state. Lazy components, like LazyColumn or LazyRow, support this use case through hoisting the LazyListState. You can learn more about this pattern in the documentation for state in lists.

Having a state parameter with a default value provided by a public rememberX function is a common pattern in built-in composable functions. Another example can be found in BottomSheetScaffold, which hoists state using rememberBottomSheetScaffoldState.

11. Observable MutableList

Next, to add the behavior of removing a task from our list, the first step is to make your list a mutable list.

Using mutable objects for this, such as ArrayList<T> or mutableListOf, won't work. These types won't notify Compose that the items in the list have changed and schedule a recomposition of the UI. You need a different API.

You need to create an instance of MutableList that is observable by Compose. This structure lets Compose track changes to recompose the UI when items are added or removed from the list.

Start by defining our observable MutableList. The extension function toMutableStateList() is the way to create an observable MutableList from an initial mutable or immutable Collection, such as List.

Alternatively, you could also use the factory method mutableStateListOf to create the observable MutableList and then add the elements for your initial state.

  1. Open WellnessScreen.kt file. Move getWellnessTasks method to this file to be able to use it. Create the list by calling getWellnessTasks() first and then using the extension function toMutableStateList you learned before.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. Modify WellnessTasksList composable function by removing the list's default value, because the list is hoisted to the screen level. Add a new lambda function parameter onCloseTask (receiving a WellnessTask to delete). Pass onCloseTask to the WellnessTaskItem.

There's one more change you need to make. The items method receives a key parameter. By default, each item's state is keyed against the position of the item in the list.

In a mutable list, this causes issues when the data set changes, since items that change position effectively lose any remembered state.

You can easily fix this by using the id of each WellnessTaskItem as the key for each item.

To learn more about item keys in a list, check out the documentation.

WellnessTasksList will look like this:

@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier = modifier) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
       }
   }
}
  1. Modify WellnessTaskItem: add the onClose lambda function as a parameter to the stateful WellnessTaskItem and call it.
@Composable
fun WellnessTaskItem(
   taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = onClose,
       modifier = modifier,
   )
}

Good job! The functionality is complete, and deleting an item from the list works.

If you click the X in each row, the events go all the way up to the list that owns the state, removing the item from the list and causing Compose to recompose the screen.

47f4a64c7e9a5083.png

If you try to use rememberSaveable() to store the list in WellnessScreen, you'll get a runtime exception:

This error tells you that you need to provide a custom saver. However, you shouldn't be using rememberSaveable to store large amounts of data or complex data structures that require lengthy serialization or deserialization.

Similar rules apply when working with Activity's onSaveInstanceState; you can find more information in the Save UI states documentation. If you want to do this, you need an alternative storing mechanism. You can learn more about different options for preserving UI state in the documentation.

Next, we'll look at ViewModel's role as a holder for the app's state.

12. State in ViewModel

The screen, or UI state, indicates what should display on the screen (for example, the list of tasks). This state is usually connected with other layers of the hierarchy because it contains application data.

While the UI state describes what to show on the screen, the logic of an app describes how the app behaves and should react to state changes. There are two types of logic: the UI behavior or UI logic, and the business logic.

  • The UI logic relates to how to display state changes on the screen (for example, the navigation logic or showing snackbars).
  • The business logic is what to do with state changes (for example making a payment or storing user preferences). This logic is usually placed in the business or data layers, never in the UI layer.

ViewModels provide the UI state and access to the business logic located in other layers of the app. Additionally, ViewModels survive configuration changes, so they have a longer lifetime than the Composition. They can follow the lifecycle of the host of Compose content—that is, activities, fragments, or the destination of a Navigation graph if you're using Compose Navigation.

To learn more about architecture and UI layer, check the UI layer documentation.

Migrate the list and remove method

While the previous steps showed you how to manage the state directly in the Composable functions, it's a good practice to keep the UI logic and business logic separated from the UI state and migrate it to a ViewModel.

Let's migrate the UI state, the list, to your ViewModel and also start extracting business logic into it.

  1. Create a file WellnessViewModel.kt to add your ViewModel class.

Move your "data source" getWellnessTasks() to the WellnessViewModel.

Define an internal _tasks variable, using toMutableStateList as you did before, and expose tasks as a list, so it's not modifiable from outside the ViewModel.

Implement a simple remove function that delegates to the list's builtin remove function.

import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel

class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask>
        get() = _tasks


   fun remove(item: WellnessTask) {
       _tasks.remove(item)
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. We can access this ViewModel from any composable by calling the viewModel() function.

To use this function, open the app/build.gradle.kts file, add the following library, and sync the new dependencies in Android Studio:

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")

Use version 2.6.2 when working with Android Studio Giraffe. Else check the latest version of the library here.

  1. Open the WellnessScreen. Instantiate the wellnessViewModel ViewModel by calling viewModel(), as parameter of the Screen composable, so it can be replaced when testing this composable, and hoisted if required. Provide WellnessTasksList with the task list and remove function to the onCloseTask lambda.
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier, 
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCloseTask = { task -> wellnessViewModel.remove(task) })
   }
}

viewModel() returns an existing ViewModel or creates a new one in the given scope. The ViewModel instance is retained as long as the scope is alive. For example, if the composable is used in an activity, viewModel() returns the same instance until the activity is finished or the process is killed.

And that's it! You've integrated the ViewModel with part of the state and business logic with your screen. Since the state is kept outside of the Composition and stored by the ViewModel, mutations to the list survive configuration changes.

ViewModel won't automatically persist the state of the app in any scenario (for example, for system-initiated process death). For detailed information about persisting your app's UI state check the documentation.

Migrate the checked state

The last refactor is to migrate the checked state and logic to the ViewModel. This way the code is simpler and more testable, with all state managed by the ViewModel.

  1. First, modify the WellnessTask model class so that it's able to store the checked state and set false as default value.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
  1. In the ViewModel, implement a method changeTaskChecked that receives a task to modify with a new value for the checked state.
class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       _tasks.find { it.id == item.id }?.let { task ->
           task.checked = checked
       }
}
  1. In WellnessScreen, provide the behavior for the list's onCheckedTask by calling the ViewModel's changeTaskChecked method. The functions should now look like this:
@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier, 
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCheckedTask = { task, checked ->
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task ->
               wellnessViewModel.remove(task)
           }
       )
   }
}
  1. Open WellnessTasksList and add the onCheckedTask lambda function parameter so that you can pass it down to the WellnessTaskItem.
@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCheckedTask: (WellnessTask, Boolean) -> Unit,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(
       modifier = modifier
   ) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(
               taskName = task.label,
               checked = task.checked,
               onCheckedChange = { checked -> onCheckedTask(task, checked) },
               onClose = { onCloseTask(task) }
           )
       }
   }
}
  1. Clean up WellnessTaskItem.kt file. We no longer need a stateful method, as the CheckBox state will be hoisted to the List level. The file only has this composable function:
@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -> Unit,
   onClose: () -> Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = "Close")
       }
   }
}
  1. Run the app and try to check any task. Notice that checking any task doesn't quite work yet.

1d08ebcade1b9302.gif

This is because what Compose is tracking for the MutableList are changes related to adding and removing elements. This is why deleting works. But it's unaware of changes in the row item values (checkedState in our case), unless you tell it to track them too.

There are two ways to fix this:

  • Change our data class WellnessTask so that checkedState becomes MutableState<Boolean> instead of Boolean, which causes Compose to track an item change.
  • Copy the item you're about to mutate, remove the item from your list and re-add the mutated item to the list, which causes Compose to track that list change.

There are pros and cons to both approaches. For example, depending on your implementation of the list you're using, removing and reading the element might be costly.

So let's say, you want to avoid potentially expensive list operations, and make checkedState observable as it's more efficient and Compose-idiomatic.

Your new WellnessTask could look like this:

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))

As you saw before, you can use delegated properties, which results in a simpler usage of the variable checked for this case.

Change WellnessTask to be a class instead of a data class. Make WellnessTask receive an initialChecked variable with default value false in the constructor, then we can initialize the checked variable with the factory method mutableStateOf and taking initialChecked as default value.

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

class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}

That's it! This solution works, and all changes survive recomposition and configuration changes!

e7cc030cd7e8b66f.gif

Testing

Now that the business logic is refactored into the ViewModel instead of coupled inside composable functions, unit testing is much simpler.

You can use instrumented testing to verify the correct behavior of your Compose code and that UI state is working properly. Consider taking the codelab Testing in Compose to learn how to test your Compose UI.

13. Congratulations

Good job! You've successfully completed this codelab and learned all the basic APIs to work with state in a Jetpack Compose app!

You learned how to think about state and events to extract stateless composables in Compose, and how Compose uses state updates to drive change in the UI.

What's next?

Check out the other codelabs on the Compose pathway.

Sample apps

  • JetNews demonstrates the best practices explained in this codelab.

More documentation

Reference APIs