State hoisting vs Defer reads in Jetpack Compose

Ranbir Singh
4 min readOct 11, 2022

--

Photo by Tao Yuan on Unsplash

To understand State hoisting and Deffer Reads, you need to understand what State is in Jetpack Compose.

The State is nothing but data at a given time. Let’s take an example of a switchboard at home; it has two forms, On and Off. The device reacts to those states of the switches.

In Compose, we use the same pattern with a “mutable state of”

now, what is a mutable state?

A mutable state is an observable similar to these (LiveData, Flow, RxJava2), which emits a value on a value change.

In the physical world, we have a switchboard that can store the switch’s last value, but in the software, we need to keep the previous value of the switch.

We use the “remember” keyword to store previous values in memory. If we don’t use “remember” with composable State, Our data switch in the software will not be updated because we don’t know the last State of the data. We need the previous value to compare with it the new one.

var name by remember { mutableStateOf("") }

In the above line of code, we are saving and observing the value of the next update in the following State.

textState will change with the future data updates in our case while we are typing in the text field

@Composable
fun TextFieldDemo() {
Column(Modifier.padding(16.dp)) {
val textState = remember {mutableStateOf(mutableStateOf(""))}
TextField(
value = textState.value,
onValueChange = { textState.value = it }
)
Text("The textfield has this text:textState.value.text)
}
}

But how is the text field updating itself with the updated State?

Initial composition: Composebale will run the first time.

Recomposition: Composable will rerun to show the change in data.

Recomposition update “Composable” according to state updates on data at any given time.

Why do we need State hoisting or Defer reads?

Unidirectional data flow is a technique used in functional reactive programming. It is also known as one-way data flow.

We also use observable patterns in Jetpack-Compose to manage our states(MutableStateof, LiveData, Flow, RxJava2)

That means the data has one way to be transferred to other parts of the components. Child components cannot update the data coming from the parent component, Which is based on a Single source of truth.

Official Doc: State hoisting in Compose is a pattern of moving State to a composable’s caller to make a composable stateless.

We are using State hoisting to make a parent-composable single source of truth for the child composable.

In the source code below, we have moved the State downward, and events are going upward.

The benefit of State hoisting is that child composable will become reusable and decoupled (Single source of truth). Now the child composable is a stateless composable, and the management of the State is moved to the parent composable, making our State management easy and code less prone to errors.

@Composable
fun RootComposable() {
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") }
)
}
}

Suppose there is continues or unexpected changes in the State that will cause another issue. We will handle that situation with Defer reads.

What is Defer reads? When should we use it?

Defer reads are State hoisting to handle an exception of state flow in an ideal scenario, which is if there is a change in the State while passing data between two composable.

Now the source of truth changed for the child components. The closest parent composable is making changes in the State.

In the source code below, the State is constantly changed, which might add unwanted recompositions in the flow.

@Composable
fun SecondRootComposable() {

Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)

Title(snack = snack,scroll=scroll.value)

}
}

@Composable
private fun Title(snack: Snack, ,scroll: Int) {
val offset = with(LocalDensity.current) { scroll.toDp() }

Column(
modifier = Modifier
.offset(y = offset)
) {
// ...
}
}

We should directly pass our State to targeted components; in this case, we use lambda functions to give value in modifiers.


@Composable
fun SecondRootComposable() {

Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)

Title(snack = snack, scrollProvider = {scroll.value })

}
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
Column(
modifier = Modifier
.offset { IntOffset(x = 0, y = scrollProvider()) }
) {

}
}

It will only update a specific part of that composable, and the whole composable will not rerun. The primary condition following Defer reads is if there are partial calculations in parent and child composable. If there is no change, use regular state hoisting.

Links:

State Hoisting: https://developer.android.com/jetpack/compose/state#state-hoisting

DeferReads: https://developer.android.com/jetpack/compose/performance#defer-reads

--

--

Ranbir Singh
Ranbir Singh

Written by Ranbir Singh

Android Team Lead @ex-Intree | Software Architect | Open Source Software Engineer. https://github.com/AndroidPoet

Responses (3)