Hi Android Devs, this article can be counted as sequel or detailed explanation of my previous article: Why I love Jetpack Compose and what makes it different from other declarative frameworks
I assume you have some knowledge in React also as I will mention some of the React’s terminologies. But you can still continue as I used React just for comparison.
A Quick recap: In the linked article, I have added my observation of Compose State updates by comparing it with React.
There I went to a conclusion that React will re-render the whole component when a state inside that component changes. But Jetpack Compose intelligently reduces recomposition of Composable that is not reading the changed state even though the state is declared in that Composable.
TL/DR:
- Composable can be skipped if the parameters is Stable and not changed.
- Compose knows what to Skip and what to Recompose
- Compose compiler reports helpful to see the Annotations on classes
Here’s some new terms to learn, Recomposition.
Recomposition: Whenever a state is changed the Composable functions will be re-executed with new state. It can be seen as similar to React’s Re-rendering.
Recomposition itself skips lambdas that is not using the state.
@Composable
fun MyUI(data: UIState) {
Column { // Column will not recomposed
Text(text = data.title) // Text will recompose
}
}
Lets take an Example to see the Recomposition in action.
See the below snippets,
For better understanding see below layout structure
EditUI will create a copy of UIState everytime the text changes.
DisplayUI will show these state in a separate Text.
Lets run the app and see how the compositions happening.
We can see the recompositions using the Layout Inspector
in Android Studio. (Menu > Tools > Layout Inspector) and enable “Show Recomposition counts”
In Layout Inspector, On left most pane shows the Composables, then the count of Recompositions, then the count of Skips.
Its evident from the below Video that if the name state changes, only Composables that read name state Recomposed while others are Skipped.
In the layout inspector, we can see the count of Recomposition increases for Name TextField and Name Text.
In the layout inspector, we can see the count of Recomposition increases for Title TextField and Title Text.
Wonderful. But did you noticed that we are actually creating a new instance of UIState, every time the title or name state changes. It needed so that Compose knows that this instance has changed and triggers the recomposition. (Same as React’s object).
How Compose knows to recompose and skip the composables?
The Answer lies in the compose compiler itself.
Compose Compiler
The compose compiler marks all @Composable
annotated functions as Skippable or Restartable. It can be both, either or neither.
Skippable: If the compiler marks a composable as skippable, Compose can skip it during recomposition if all its arguments are equal with their previous values.
Restartable: A composable that is restartable serves as a “scope” where recomposition can start. In other words, the function can be a point of entry for where Compose can start re-executing code for recomposition after state changes.
The compose compiler marks all State
objects as either Immutable or Stable. It can be both, either or neither.
Immutable: Compose marks a type as immutable if the value of its properties can never change and all methods are referentially transparent. Note that all primitive types are marked as immutable. These include
String
,Int
, andFloat
.Stable: Indicates a type whose properties can change after construction. If and when those properties change during runtime, Compose becomes aware of those changes.
Lets on with our above example, what are the categories our classes resides. Using Compiler reports, we can see these annotations in our classes: https://developer.android.com/jetpack/compose/performance/stability/diagnose
After following steps from above link, I got three report files: app_release-classes.txt
app_release-composables.txt
app_release-composables.csv
.
I found,
// From app_release-classes.txt
stable class UIState {
stable val name: String
stable val title: String
<runtime stability> = Stable
}
// From app_release-composables.txt
restartable skippable scheme("[...]") fun EditUI()
restartable skippable scheme("[...]") fun DisplayUI(
stable uiState: UIState
)
Here we can see that, our UIState is marked as Stable, so if any state changes, compose knows it. Our two composables are marked as restartable and skippable. But Why?
If you look into the UIState, all the properties are immutable and also val
.
So the composables that uses this UIState also marked as skippable.
Let’s add list type with val
. I hope this wouldn’t change the stability as List is immutable.
// UI State
data class UIState(val name: String, val title: String, val list: List<String> = listOf())
Now, the compiler reports looks like,
unstable class UIState {
stable val name: String
stable val title: String
unstable val list: List<String>
<runtime stability> = Unstable
}
restartable skippable scheme("[...]") fun EditUI()
restartable scheme("[...]") fun DisplayUI(
unstable uiState: UIState
)
What? Now the list itself unstable, class is unstable, so the composable is non-skippable. But the list is immutable right? Right? No!
For Compose, it is unstable data type. Even if we use val
, technically we can change the values, add, delete the list. While initializing the class we can use,
UIState("", "", mutableListOf()))
It allows us to pass mutableList, so compose is right.
What if we want to make it stable but we want the list type?
Immutable Collections to the rescue.
- Add the dependency
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
- Use ImmutableList in place of List
data class UIState(val name: String, val title: String, val list: ImmutableList<String> = persistentListOf())
Now let’s see the reports.
stable class UIState {
stable val name: String
stable val title: String
stable val list: ImmutableList<String>
<runtime stability> =
}
restartable skippable scheme("[...]") fun EditUI()
restartable skippable scheme("[...]") fun DisplayUI(
stable uiState: UIState
)
Oh nice, Now its Stable.
So, Stable state makes the Composables Skippable. Skippable Composables can be skipped if no state is changed.
Let me know in the comments if you find other things the compose compiler uses to Optimize.
Source: https://developer.android.com/jetpack/compose/mental-model
If you come so far here, take a moment to follow me on Twitter
Original Post: https://sanjaydev.tech/blog/jetpack-compose-internals-part-1-performance-732f24c07f36