Understanding Strong Skipping in Jetpack Compose
1. Introduction to Strong Skipping
In Jetpack Compose, strong skipping mode optimizes the recomposition process by ensuring
that composables are only recomposed when necessary. It skips recomposing composables
that haven't changed and avoids unnecessary work. This is generally beneficial for performance
but can introduce issues in certain scenarios where composables rely on accidental side effects,
such as when mutable state changes are not explicitly tracked.
Previously, some code might have "worked by accident," relying on implicit triggers for
recomposition (e.g., mutable state changes not being properly tracked). With strong skipping
enabled, these accidental triggers may no longer fire, causing unexpected behavior or failures
to re-render UI elements.
2. Problem with the Code Example
@Composable
fun MyToggle(enabled: Boolean) {}
@Composable
fun MyList(list: List<String>) {}
@Composable
fun MyScreen() {
var list by remember { mutableStateOf(mutableListOf("Foo"))
}
var toggle by remember { mutableStateOf(false) }
MyToggle(toggle)
MyList(list)
Button(
onClick = {
list.add("Bar")
toggle = !toggle
}
) { Text("Toggle") }}
Why This Code Worked Previously
Before enabling strong skipping, the code worked because:
1. The toggle variable was being changed, which triggered a recomposition of MyScreen.
2. During this recomposition, the MyList composable was also recomposed because the list
was stored in a mutable state (mutableStateOf). The add("Bar") operation in the Button's
onClick handler would trigger the UI to re-render, even if the list itself wasn't explicitly
marked as changed.
However, this behavior was not ideal and relied on accidental side effects: the change to the
toggle was causing the recomposition of the entire screen, which in turn caused the MyList
composable to re-render.
Why It Doesn't Work with Strong Skipping
When strong skipping is enabled:
● Jetpack Compose optimizes recomposition by skipping composables whose state has
not explicitly changed.
● In this case, the list is a mutableListOf, which is a mutable object. Modifications to
mutable objects (like calling add()) do not trigger recomposition automatically.
● The toggle state change no longer forces the MyList composable to recompute, as the
state of list itself has not changed according to Compose's recomposition rules.
Thus, the MyList composable will be skipped during recomposition, and the UI will not update to
reflect the changes in the list.
3. How to Fix It
To ensure that MyList is properly recomposed after the list is modified, you need to ensure that
the state holding the list is observed correctly. This can be done by using SnapshotStateList or
using immutable data structures to trigger recomposition.
Using SnapshotStateList
SnapshotStateList is a type of list that supports composition and recomposition tracking,
meaning that it will trigger recomposition when its contents change.
Here’s how to modify the code to use SnapshotStateList:
@Composable
fun MyToggle(enabled: Boolean) {}
@Composable
fun MyList(list: List<String>) {}
@Composable
fun MyScreen() {
// Use SnapshotStateList for mutable lists
var list by remember {
mutableStateOf(SnapshotStateList<String>()) }
var toggle by remember { mutableStateOf(false) }
MyToggle(toggle)
MyList(list)
Button(
onClick = {
list.add("Bar")
// Modifying the SnapshotStateList triggers recomposition
toggle = !toggle
}
) { Text("Toggle") }
}
Explanation of the Fix
● SnapshotStateList is a special kind of list that integrates with Compose’s state
management system. When you modify its contents (e.g., by adding or removing
elements), Compose can detect these changes and trigger the necessary recomposition.
● By using SnapshotStateList, the recomposition of MyList will now be correctly triggered
when the list is modified.
Alternative: Using Immutable Lists
If you prefer working with immutable lists, you can replace the
mutableStateOf(mutableListOf(...)) with an immutable list and use mutableStateOf to track the
new list after every modification:
@Composable
fun MyScreen() {
// Use immutable List for better Compose integration
var list by remember { mutableStateOf(listOf("Foo")) }
var toggle by remember { mutableStateOf(false) }
MyToggle(toggle)
MyList(list)
Button(
onClick = {
// Create a new list, triggering recomposition
list = list + "Bar"
// This creates a new list instance
toggle = !toggle
}
) { Text("Toggle") }
}
Explanation of This Approach
● Here, instead of modifying the existing list, a new list is created each time you modify it
(list + "Bar"). This change to the list triggers a recomposition of MyList because the
mutableStateOf will now hold a new instance of the list.
● This ensures that Compose detects the change and re-renders the composable as
needed.
4. Conclusion
With strong skipping mode enabled, composables that rely on mutable objects or accidental
state changes may not trigger recompositions as expected. This behavior can be problematic,
especially when mutable lists or other mutable state objects are involved.
To avoid these issues:
● Use SnapshotStateList for mutable lists, which will correctly trigger recomposition when
the list is modified.
● Alternatively, use immutable data structures and replace the entire list when it changes,
which will trigger recomposition naturally.