Cant find the role you’re looking for?
Sign up to be the first to know about future job openings at GetYourGuide
Stay notified of new jobsCopyrighted 2008 – 2023 GetYourGuide. Made in Zurich & Berlin.
Sagar Khurana, Android Engineer at GetYourGuide, delves into the Compose Compiler Metrics Plugin, offering insights on immutable and mutable objects, and providing practical solutions to common performance issues. Learn how to leverage stability concepts to optimize your composables for a seamless user experience.
{{divider}}
This article will explore the crucial concept of Stability in Compose. We will understand the factors that cause instability and, most importantly, how to stabilize it. This knowledge will help you write more performant composables. So, let's dive in.
If you haven't done so yet, I recommend reading Part 1, where we discussed Jetpack Compose's phases, along with the concepts of restartability and skippability, and their benefits for creating efficient composables.
The Compose Compiler plugin is a powerful tool that generates reports/metrics around specific compose-related concepts. This theoretical and practical information allows us to peek behind the curtain and use it to our advantage in writing performant composables. Now that we understand its potential, let’s try to generate the reports and see how they can benefit us.
Compose compiler reports are not enabled by default. They can be enabled via a compiler flag. Add the following script to the root build.gradle to allow reports for all modules.
Run the following command to generate the reports
⚠️ Warning: Run the above command on a release build to ensure accurate results.
⚠️ Warning: You may need to run the command above with ––rerun–tasks, to ensure that the Compose Compiler runs, even when cached.
Running the command above generates four files in each module's build directory. Whatever module has the compose code is present.
Output files are:
From the above output, our primary focus should be on <modulename>-classes.txt and <modulename>-composables.txt. If you carefully examine the output from these two files, you’ll come across terms like stable, unstable, restartable, skippable, @static, @dynamic, and runtime stability. Don't worry if it seems overwhelming at first. We'll go through these terms, ensuring you understand their true meaning. Let’s start with the <modulename>-classes.txt. Bear with me; we'll move on to practical examples once you grasp these concepts.
It generates reports about the class type. Outputs about the class Runtime Stability, Stable, or Unstable and tells you stability about its param types.
Stability depends on other dependencies, which will be resolved at runtime.
There are two types:
This class holds a mutable data type that does not notify Compose Runtime upon a change in data. Compose Runtime is unable to validate whether these have changed or not. That’s why Stable inference algorithms treat it as Unstable. If one or more class parameters are unstable, then the stability of that class is also considered Unstable.
Let’s revisit it with their associated examples because it's essential to understand.
Primitives (Int, Long, Boolean, etc.), Strings, and function types, such as lambda, are treated as Stable types.
MutableState object, i.e. mutableStateOf. They are treated as Stable types.
We will later explore all these examples of why a particular type is considered unstable with a practical code example. This is to give a high level of what we will explore.
Before we proceed forward, you must also know about Restartable and Skippable. We’ve already covered them in Part 1. Check that out, but in short.
This provides a scope for initiating recompositions. Most composables are restartable, except inline ones, such as Column and Row Composable.
Composables can skip recomposition if all parameters are stable. By default, they're skippable unless the parameters are unstable.
Using the knowledge above, let's look at some practical problem sets.
Code Explanation
This code has a Column with Checkbox and Contacts composable, which takes a List<ContactInfo>. On Click of Checkbox, which inverses the value of isChecked Boolean, the checkbox's value is inverted.
Behavior
The code looks correct. Let's check the recomposition counts.
On Click of Checkbox, it recomposing the Contacts(...) composable as its list is not changed at all 🤔
Cause
To understand the cause, let's look at the compiler reports for the Contacts Composable.
So, the contacts parameter is treated as unstable. If you are wondering why, 🤔 it’s because Composable takes a List (Collection Type) as a param, an interface. The Compose compiler cannot be sure of the immutability of this class as it just sees the declared type and, as such, declares it as unstable as the implementation could still be mutable. So, Compose recomposes the Contacts composable on every recomposition because Compose is sure about whether the unstable params changed or not.
Solution #1 - Stable Annotations
To stabilize the param, i.e., the Collection type (List, Set, etc.), use a wrapper data class annotated with a stable annotation (@Immutable or @Stable) wrapping a List.
Requirements for a type to be considered stable:
@Immutable:
@Stable:
Code Change
Changed the Contacts Composable param to ContactInfoOneList data class, which is a wrapper class annotated with @Immutable
The compiler reports that after the code changes 🎉, It’s stable, which means there are no more unwanted recompositions due to unstable params.
Result
No more unwanted recompositions 🕺
Solution #2 - Kotlinx.collections.immutable
To stabilize the param, i.e., Collection type (List) using an immutable list like kotlinx.collections.ImmutableList
Note: The minimum Jetpack Compose version required is version 1.2 for the compiler to consider the kotlinx.collections.ImmutableList as stable.
Code Change
Change Contacts Composable param to ImmutableList<ContactInfoTwo>, Which is treated as Stable.
Code from Caller, build a list using the persistentListOf function.
The compiler reports that after the code changes 🎉, It’s stable, which means there are no more unwanted recompositions due to unstable params.
Result
Refer to Figure PS1 to see the output from the layout inspector.
Solution #3 - Compose Stability Config File
With the Compose Compiler 1.5.5 release, a configuration file of classes can be defined there, which will be considered stable. The configuration file is a plain text file with one class per row. An example configuration is shown below.
Code Change
To use the Compose Stability Config File, we must define a compose_compiler_config.conf at the project's root, as described in Figure PS2. It can be provided to each module separately, but I added it to the subprojects. So, there is one file that we need to maintain, and all the modules can benefit from it.
That’s all we need to change on the composables side. Now, the Compose compiler will consider these classes Stable.
The compiler reports that after the code changes 🎉, It’s stable, which means there are no more unwanted recompositions due to unstable params.
Result
Refer to Figure PS1 to see the output from the layout inspector.
Now, bye-bye to all the refactoring comes wrapping the data class with annotations or the Kotlinx persistent list.
⚠️Warning: These configurations don't make a class stable. Instead, you opt into a contract with the compiler by using them. Incorrectly configuring a class could cause recomposition to break.
Code Explanation
In this code, we have a Column with Checkbox and Contact Composable, which takes a ContactInfo data class. On Click of Checkbox, isChecked Boolean is changed, triggering the recomposition.
Behavior
The code looks correct. Let's check the recomposition counts.
On Click of Checkbox, its recomposing is Contacts(...) composable as ContactInfo is not changed at all 🤔
Cause
Let’s take a look at the compiler reports of the Contact Composable. The ContactInfo param is unstable because its params are defined as "var," which means they're mutable and can be changed at runtime. Thus, its runtime stability is Unstable, making the composable non-skippable.
There’s also another option to see what the cause (which in this case is an unstable param) of recompositions is using the debugger; this is only available in Android Studio Hedgehog | 2023.1.1 where it shows Compose state information in the debugger and which explains about if the variable is
This tool is a lifesaver for understanding the reason for recomposition.
Solution
Make your param as "val" instead of "var.
Code Change
Changed keyword from var to val in ContactInfo data class, which made the ContactInfo data class stable and Contact composable skippable as well.
Compiler Report of ContactInfo data class and Contact composable
Result
No more unwanted recompositions 🕺
Code Explanation
In this code, we have a Column with Checkbox and ContactModule composable, which takes a ContactInfo data class that is present in another module called domain; on Click of a checkbox, which inverses the value of isChecked Boolean, which then inverters the value of the checkbox and triggers the recomposition.
Behavior
Looks all good, right? But no. Why, on Click of Checkbox, its recomposing is ContactModule(...) composable as ContactInfo is not changed at all 🤔
Cause
Composable is taking a data class, which is in a separate module. Thus, the compiler cannot infer its stability. As such, it declares it unstable.
Solution
Code Explanation
This code has a column with a checkbox and a Button that can be combined. When the checkbox is clicked, the isChecked value is inverted, and recompositions trigger.
Behavior
The code looks correct. Let's check the recomposition counts.
On Click of Checkbox, it's recomposing the Button(..) Composable, but no params of the Button Composable is changed 🤔
Cause
Composable is taking a lambda as a parameter and accessing the unstable type inside that lambda.
Solution
There are two solutions:
Solution #1 - Use a Method References
Why it works? Prevent the creation of a new class, which in turn references an unstable type class. Method references are @Stable functional types.
⚠️It is not guaranteed that Unstable Lambda will be fixed if it is inside an unstable class. In such cases, the solution is to remember Lambda.
Code Change
Inside the onClick param of Button composable, instead of passing a lamba, passing the method reference.
Result
No more unwanted recompositions
Solution #2 - Remembered Lambdas.
Remember the lambda instance between recompositions. Ensure the same instance of the lambda.
💡 When remembering a lambda, pass any captured variables as keys to remember so that the lambda will be recreated if those variables change.
Code Change
Now, inside the Button Composable onClick param, pass the remembered lambda value.
Result
No more unwanted recompositions. Refer to Figure PS3.
GetYourGuide has been adapting changes in its code base to improve the performance of its Compose code. For instance, we recently optimized our date picker so that only the changed cells are recomposed upon clicking, instead of the previous method, which recomposed almost 50 cells. We utilized the information mentioned above to make this possible.
Before Optimization
After Optimization
By the time this article was published, the Strong Skipping concept had been released, which changed this conservative approach to recomposing composables. Read it to understand how it can help you write performant composables out of the box.
The above samples are in this Github Repo - hellosagar/ComposePerformance. Special thanks to Sagar Viradya, Vaibhav Jaiswal, Piyush Pradeepkumar, Shreyas Patil, Kasem SM, Lavina Dhingana, Alireza, Milan Jovic, Benedict Pregler and Volodymyr Machekhin for reviewing the article.