Blog Infos
Author
Published
Topics
, , , ,
Author
Published

ai generated image

Jetpack Compose is a declarative UI, so it follows the flow of state change → recomposition → UI update.

However, mishandling state can cause unnecessary recompositions, which directly leads to performance degradation.

This article compares techniques for reducing recompositions using Wrong examples and Correct examples.

How to read the state within the narrowest possible range

Reference From Android Developer

Wrong

 

@Composable
private fun Parent() {
var count by remember { mutableIntStateOf(0) }
// Parent reads state directly → Parent undergoes full recomposition
ChildA(count = count)
}

@Composable
private fun ChildA(count: Int) {
Text("Count: $count")
}

 

In Jetpack Compose, we often declare state using `by`.
The `by` keyword signifies property delegation, meaning it delegates access to the `value` of the State object.
In other words, simply referencing the variable internally calls `state.value`.

Compose tracks these `value` reads and automatically recomposes the composable that read the value when the state changes.
Therefore, if you’re passing a state down to a child without the parent composable directly using it,
you must structure the parent so it doesn’t “read” that state.
Otherwise, every composable inside the parent could be exposed to unnecessary recomposition.

Correct
@Composable
fun Parent() {
val count = remember { mutableStateOf(0) }

// Pass a lambda that reads the state
ChildB(count = { count.value })
// Pass without reading the state
ChildC(count = count)
}

@Composable
fun ChildB(count: () -> Int) {
Text("Count: ${count()}") // Here, the state is actually read.
}

@Composable
fun ChildC(count: State<Int>) {
Text("Count: ${count.value}") // Here, the state is actually read.
}

 

There are two ways to avoid this issue.
First, pass the State<T> object itself to the child instead of reading the state directly. If the parent does not call `state.value`, the parent is not traced as reading the state, and only when the child composable needs it does it read `state.value`, triggering a recomposition.

Second, pass the state access via a lambda. Since lambdas are functions (code, not values), any code inside the lambda that references `state.value` only executes when the lambda is actually called. This delays the state read to the point of invocation, ensuring it’s read only where it’s actually needed.

Why is this method possible?

Compose’s state is managed by the Snapshot System,
and all state read and write operations are tracked per snapshot.

The Snapshot System detects state changes
and informs the Compose Runtime that the composable (the scope of execution for a Composable function) that read that state must be re-

xecuted.
In other words, snapshots serve to track triggers for recomposition caused by state changes.

For this tracking to be possible,
when state is read, the read operation must be registered in the global snapshot.
Through this process, Compose records “which composable read which state,”
enabling it to recompose only the appropriate scope later when that state changes.

// Inside the mutableStateOf implementation
override var value: T
        get() = next.readable(this).value
        set(value) =
            next.withCurrent {
                if (!policy.equivalent(it.value, value)) {
                    next.overwritable(this, it) { this.value = value }
                }
            }

 

You will see the value in this code. This value is the `state.value` we are using.

Let’s take a closer look at the getter.

public fun <T : StateRecord> T.readable(state: StateObject): T {
val snapshot = Snapshot.current
// This is where the snapshot system indicates that the state has been read.
snapshot.readObserver?.invoke(state)
return readable(this, snapshot.snapshotId, snapshot.invalid)
?: sync {
val syncSnapshot = Snapshot.current
@Suppress("UNCHECKED_CAST")
readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.invalid)
?: readError()
}
}

 

Calling `state.value` triggers the snapshot’s readObserver,
which informs the snapshot system where the state is being read from.

Through this process, Compose records “which composable read which state,”
and when that state changes, it can recompose only that specific part.

Therefore, to prevent unnecessary recompositions,
you should only read state at points where it is truly needed.
In other words, simply adjusting where state is read can effectively minimize the scope of recompositions.

How to handle rapidly changing State
wrong
val showTopBar = lazyListState.firstVisibleItemIndex > 20

 

We sometimes derive one state from another.

If the state being used changes rapidly, like LazyListState, recompositions can occur quite frequently.

Android Studio also displays warnings directly.

correct
// case of derivedStateOf
val showTopBar by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 20 }
}

// case of snapshotFlow
var showTopBar by remember { mutableStateOf(false) }

LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.firstVisibleItemIndex }
.map { it > 20 }
.distinctUntilChanged()
.collect { visible -> showTopBar = visible }
}

 

 

When dealing with rapidly changing states, you can use two methods.
The first is using derivedStateOf, and the second is using snapshotFlow.

Using derivedStateOf allows you to define a calculation block internally. When values change within that block, the state updates automatically.
In other words, the necessary calculation results are generated automatically based on state changes.

In contrast, snapshotFlow converts the state into a Flow, enabling us to process the data in the way we want.
Because we can define the calculation logic directly, we can flexibly implement various processing steps based on state changes.

When should I use which method?

When simply deriving state through calculations, it is recommended to use `derivedStateOf`.
However, when asynchronous processing is required alongside state changes, or when additional effects like toasts need to be triggered, using `snapshotFlow` is more appropriate.

In other words, if only the derived state is needed, choose `derivedStateOf` if you also need to handle side effects resulting from state changes, choose `snapshotFlow`.

How to Defer state reads as long as possible
wrong
Text(
modifier = Modifier
.align(Alignment.Center)
.offset(y = 60.dp * animatedValue) // The state is read at the time of composition phase.
.scale(animatedValue), // The state is read at the time of composition phase.
text = "Animate"
)

 

We often use state and modifier to dynamically change a composable’s position, size, transparency, and so on.

However, if a rapidly changing state is read during the composition phase via modifier, unnecessary recomposition can occur frequently whenever that state changes abruptly.

correct
Text(
modifier = modifier
.offset { IntOffset(0, (60 * animatedValue).toInt()) } // read state on layout phase
.drawWithContent { // read state on draw phase
scale(scale = animatedValue) {
this@drawWithContent.drawContent()
}
},
text = "Animate"
)

// or

Text(
modifier = modifier
.offset { IntOffset(0, (60 * animatedValue).toInt()) } // read state on layoutphase
.graphicsLayer { // read state on draw phase
scaleX = animatedValue
scaleY = animatedValue
},
text = "Animate"
)

 

We can utilize methods that read state during the layout phase, like lambda Modifier,
or methods that read state during the draw phase, like `drawWithContent` or `graphicsLayer`.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

Why do we read the state after the Composition phase?

from Android Developer Document

The process of rendering the UI on the screen in Compose consists of three main stages:
Composition, Layout, and Draw.

The key point to remember here is that during the Composition stage, if the state is read and changed, a recomposition occurs.

The Composition stage builds the UI Tree based on the state.
Once the UI Tree is constructed, the Layout stage arranges its components, and the Draw stage renders it on the screen.

Therefore, reading state during the Composition stage necessitates rebuilding the UI Tree whenever the state changes, triggering a recomposition.
Conversely, reading state during the Layout or Draw phases allows updating UI properties like position, size, or opacity without triggering a recomposition.

In Closing

Recomposition does not cause as significant a performance degradation as one might expect.
However, if you pay attention to recomposition,
you can eliminate at least one potential cause of future performance degradation.

 

This article was previously published on proandroiddev.com

Menu