Managing State in Jetpack Compose

Learn the differences between stateless and stateful composables and how state hoisting can help make your composables more reusable. By Rodrigo Guerrero.

5 (2) · 1 Review

Download materials
Save for later

Jetpack Compose is the new toolkit for building user interfaces in Android. You can use Kotlin code to create UI, letting you forget about old XML layouts.

But with great power comes great responsibility. Managing the state of the UI’s components requires a different approach than with XML layouts.

In this tutorial, you’ll build an app named iState. This app has two screens: a registration form that lets you add users to a list and the list to display the registered users.

In this tutorial, you’ll learn:

  • Composable functions
  • Recomposition
  • Stateful composables
  • Stateless composables
  • State hoisting
Note: This tutorial assumes you know Jetpack Compose basics. If you’re new to Jetpack Compose, check Jetpack Compose Tutorial for Android: Getting Started.

Getting Started

Download the materials by clicking Download Materials at the top or bottom of this tutorial. Open Android Studio Bumblebee or later and import the starter project.

Below is a summary of what each package contains:

  • models: Class that represents a user.
  • ui.composables: Composables used to create the screens.
  • ui.theme: Jetpack Compose theme definition classes.

Build and run. You’ll see a screen with a FloatingActionButton.

Empty Users List

Click the button to see the registration screen.

Empty Registration Screen

Notice nothing happens if you try to interact with the form – you can’t see the text you type, change the selected radio button or display the drop-down menu to select your favorite Avenger. You’ll learn how to manage state in Jetpack Compose and make these screens work.

But first, take a moment to learn more about Jetpack Compose.

Introducing Jetpack Compose

Jetpack Compose is a declarative way to create user interfaces. Instead of creating the layout once and updating the state of each component by hand, as you’ve always done with views in XML files, Compose renders each screen from scratch. It repeats the process each time any of the values in the screen change.

To create your UI with Compose, you need to create composable functions. Composable functions are the building blocks in Compose. They can receive data, use the data to create the UI and then emit UI components that users see on the screen.

It’s time to create the first composable in this tutorial.

Open MainScreenComposables.kt. There you’ll find the empty composable UserList() that will display the list of registered users in your app.

This function receives a list of users and doesn’t have a return value. Composable functions emit UI elements so they don’t need to return anything.

In the body, add the necessary composables to display the user items on the list:

 // 1.
  LazyColumn() {
    // 2.
      items = users,
      key = { user -> }
    ) { user ->
      // 3.
Note: Add import and import androidx.compose.material.Divider at the top of MainScreenComposables.

To display a list of items, you use LazyColumn(). Several things happen in this function:

  1. LazyColumn() is a composable function that emits a column that loads its items lazily. You can execute a composable function from another composable function only. To fulfill that rule, UserList() has the Composable annotation.
  2. Use items() to assign a list of items to the column. You can also use the key attribute to add a distinctive identifier to each item in the list.
  3. For each item, emit an ItemUser() row and a Divider() which will make the list look pretty.

UserList() is free of side-effects, which means it always shows the same result with the same input data. It also doesn’t change any global variables or change state. Also, notice UserList() has a parameter set to emptyList(). That means if you don’t provide any data, the app will display an empty list.

Now open MainActivity.kt. Here you’ll find the composable function that emits the screen with all users, UserListScreen(). You don’t need to add anything at this point, but keep in mind you’ll change this code later to have a list with real users.

With these Jetpack Compose basic concepts in mind, you can create any Composable for the sample app and start learning how to handle state in Jetpack Compose. But first, it’s essential to learn how Compose works with data flows and updates the screens.

Understanding Unidirectional Data Flow

Jetpack Compose works entirely differently than XML layouts. Once the app draws a composable, it’s impossible to change it. However, you can change the values passed to each composable, which means you can change the state each composable receives.

On the other hand, a composable might generate events that can update the state. For example, your EditTextField generates an event with the text the user is typing. This event updates the composable’s state so it can show the typed text.

Compose uses the unidirectional data flow design pattern that indicates data or state only flows down while events flow up, as shown in the following diagram:

Unidirectional Data Flow

This diagram represents the UI update loop in Compose:

  1. The composable receives state and displays it on screen.
  2. An event can modify the state values and can come from a composable or from other parts in your app.
  3. The state handler, which can be a View Model, receives the event and modifies the state.

As you can see, the only way to update a composable is to redraw it. So, how can Compose know when to redraw composables? This is where recomposition enters the scene.

Learning About Recomposition

To update a composable, you need to call the composable function with the new data, thereby triggering the process of recomposition. During that process, Compose is only intelligent enough to redraw the composables whose state has changed.

Compose always tries to finish the recomposition before it needs to recompose again. However, sometimes state changes before the recomposition completes. In that case, Compose cancels the recomposition and restarts it with the new state values.

Recomposition can execute composable functions in any order. A composable function shouldn’t generate secondary effects, like changing a global variable, because Compose doesn’t guarantee the order of execution.

Composable functions can run in parallel. That’s another reason to have composable functions without side effects.

For example, suppose you change the value of a local variable within a composable. In that case, the variable could end up with an incorrect value. The recommended way to trigger side effects within a composable is to use callbacks that send events up to the state handler.

Finally, composable functions often run quietly, which means you shouldn’t perform expensive operations within them.

Now that you understand recomposition and why it’s essential, it’s time to talk about State.