The Talent500 Blog
State Hoisting in JetPack Compose 1

State Hoisting in JetPack Compose

JetPack Compose is now a part of Modern Android Development. JetPack Compose is Android’s modern toolkit from Google to build native UI. 

Many Kotlin Android developers are in love with this new way of designing UI. JetPack Compose made dealing with recycler views, navigation, and animations so easy and straightforward. 

In this article, we will be discussing the most important topic which is State Hoisting in Compose. First, let’s take a look at what is a State in JetPack Compose, and then I’ll take you through different concepts of State Hoisting. Let’s go!

What is a State in JetPack Compose?

An app’s state is any value that can change during the execution of your app. App’s state determines what is displayed in the UI. 

A few examples of state in android apps are –

  1. The most recent messages received in WhatsApp OR the most recent emails received in Gmail.
  2. Scrolling YouTube videos, Instagram reels, Twitter tweets, etc.
  3. Liking friend’s posts by pressing that Like button on Facebook.

Define State in Compose

State<T>

A type that holds a read-only value: It notifies the composition when the value changes.

MutableState<T>

It’s an extension function of the State. It allows us to update the value. When the value property is written to and changed, a recomposition of any subscribed compose scope will be scheduled. 

If value is written to with the same value, no recompositions will be scheduled.

Consider the following composable function — 

@SuppressLint(“UnrememberedMutableState”)
@Composable
fun Counter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = mutableStateOf(0)
        Text(
            text = “You’ve had ${count.value} glasses today.”,
            modifier = modifier.padding(10.dp)
        )
        Button(onClick = { count.value++ }, modifier = Modifier.padding(top = 8.dp)) {
            Text(text = “Add One”)
        }
    }
}

Whenever we press the button and increase the count, the composable function recomposes and the UI should show the updated value but nothing happens. Why is that? 

The problem is that the variable is reinitialized to 0 when the function re-executes. 

This is where Remember API comes into the picture. You can use remember inline composable function like this — 

@Composable
fun Counter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = remember { mutableStateOf(0) }
        Text(
            text = “You’ve had ${count.value} glasses today.”,
            modifier = modifier.padding(10.dp)
        )
        Button(onClick = { count.value++ }, modifier = Modifier.padding(top = 8.dp)) {
            Text(text = “Add One”)
        }
    }
}

Now the count value is stored in the composition and this value is being tracked across recompositions. 

Go to the app and press the button, and you can see that the state value is changing. The state mechanism is actually working!

So usually, remember and mutableStateOf are used together to manage states across the app at runtime.

There are a few ways to deal with State API to define states in compose.

  1. Accessing the value property of a state object every time is the one way.

val count = remember { mutableStateOf(0) }
Text(
    text = “You’ve had ${count.value} glasses today.”,
    modifier = modifier.padding(10.dp)
)

2. Another way that is more convenient than the first one is with by keyword which uses Kotlin’s delegated properties. State object should be now an var instead of an val if you are using by keyword.

var count by remember { mutableStateOf(0) }
Text(
    text = “You’ve had $count glasses today.”,
    modifier = modifier.padding(10.dp)
)

What is State Hoisting?

State Hoisting is a pattern of moving state to a composable’s caller. Compose uses the state hoisting pattern to make composable stateless.

You can use the state object if you want to update the data at runtime in UI.

The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:

  1. value: T — the current value to display
  2. onEventCallback: (T) -> Unit— an event that requests the value to change, where T is the proposed new value.  

Why is State Hoisting important?

State Hoisting has some important properties –

  1. Single source of truth: It helps to avoid bugs while dealing with states in compose. It is being ensured that there is a single source of truth by moving the state out of composable instead of duplicating it every time the state needs to be changed.
  2. Shareable: Hoisted state can be used with multiple composables. 
  3. Encapsulated: Only stateful composable that hold the state will be able to modify their state.
  4. Decoupled: The state of a stateless composable function can be stored anywhere. For example, the state value can be stored inside ViewModel.
  5. Interceptable: Callers to stateless composable have the right to decide to ignore or modify events before changing the state.

Stateless VS Stateful Composable

State Hoisting in JetPack Compose 2

Stateful Composable:

A composable that contains an internal state is a Stateful composable. 

Consider the following code — 

@Composable
fun StatefulCounter(
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier.padding(16.dp)) {

        var counter by remember {
            mutableStateOf(0)
        }

        if (counter > 0) {
            Text(
                text = “You’ve had $counter glasses today.”,
                modifier = modifier.padding(10.dp)
            )
        }

        Button(
            enabled = counter < 10,
            onClick = { counter++ },
            modifier = modifier.padding(10.dp)
        ) {
            Text(text = “Add One”)
        }

    }
}

StatefulCounter is an example of stateful composable because it holds the count state value.

These composables tend to be less reusable and harder to test.

Stateless Composable:

Composables that don’t hold any state are called Stateless composables. 

An easier way to create stateless composable is by using state hoisting. 

Consider the following code for better understanding — 

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

    var count by rememberSaveable {
        mutableStateOf(0)
    }

    StatelessCounter(count, {count++}, modifier = modifier)
}

@Composable
fun StatelessCounter(
    count: Int,
    onIncrement: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier.padding(16.dp)) {

        if (count > 0) {
            Text(
                text = “You’ve had $count glasses today.”,
                modifier = modifier.padding(10.dp)
            )
        }

        Button(
            enabled = count < 10,
            onClick = onIncrement,
            modifier = modifier.padding(10.dp)
        ) {
            Text(text = “Add One”)
        }
    }
}

StatelessCounter composable is a stateless composable because it does not hold any state. Instead, it will hoist the state provided by StatefulCounter the caller composable and hoist it. This pattern is called State Hoisting.

Stateless composables are more reusable and easier to test as well.

Remember VS Remember Savable

Remember

remember keyword can be used to store both mutable and immutable objects. If the change happens in a state’s value, it will recompose the UI widgets and show the updated UI on the screen. 

var count by remember { mutableStateOf(0) }

What will happen if you rotate the device once the state is updated? You will see that composable is initialized again with the initial state value.

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

remember helps you retain state across recompositions, but it’s not helpful in retaining state across such configuration changes.

Remember Savable

Use rememberSavable to avoid the drawback of using remember. It will help you retain state across configuration changes where the activity and process are recreated. 

It means that you can restore your UI even after an activity or process is recreated. 

var count by rememberSaveable { mutableStateOf(0) }

Now try to rotate your device again and see if the state is restored or not. You will see that the state is actually retained in the UI.

Conclusion

Amazing! Now you have an idea about state management in JetPack Compose. By the end of this article, you’ve hopefully learnt about state hoisting and some important points to remember while dealing with states in composition. 

 

0
Prachi Jamdade

Prachi Jamdade

Prachi is an Android Developer @Business Score and a Software Engineer Intern @Codemonk. She loves to write about her learnings in tech and software development. You can find her on GitHub @Prachi-Jamdade and on Twitter @prachiijamdade

Add comment