This is the fifth and last part of the Compose dribbble replicating series. While the previous parts (Part 1, Part 2, Part 3, Part 4) focused on implementing complex UI and animations, this part will be about Compose compiler metrics. You will see how to gather information with metrics and how it can in with fixing performance issues.
In Jetpack Compose 1.2.0, a new feature was added to the Compose compiler that can display various performance metrics during the build. This is a great tool to find potential performance issues.
First step is to configure the metrics - to do this, add the following to your build.gradle
file:
Now run the build with the following options enabled (please note that the build must be compiled in release mode).
./gradlew assembleRelease -PmusicApp.enableComposeCompilerReports=true
After the build, the compose_metrics
folder will hold the files we need to look at.
app_release-classes.txt
- class stability information.app_release-composables.txt
- information about whether the method is restartable or skippable.app_release-composables.csv
- the same information about the methods, only as a table.app_release-module.json
- general metric information about the project.
First let's review app_release-module.json
and look at composables metrics.
skippableComposables
- a composition function is "skipped" if the framework skips its call when the function parameters do not change.
restartableComposables
- a composition function can be restarted (re-invoked) independent of the function parameters changes.
So, according to the statistics, we have functions that are restarted but not skipped. This potentially means they are restarted for no reason - if the parameters didn't change, the function could potentially be skipped and not recalculated. As the official documentation puts it:
If you see a function that is restartable but not skippable, it’s not always a bad sign, but it sometimes is an opportunity to do one of two things:
- Make the function skippable by ensuring all of its parameters are stable
- Make the function not restartable by marking it as a
@NonRestartableComposable
Let's now look at the app_release-composables.csv
table. Sorting the table by skipabble
lets us determine restartable but not skippable methods. These are the ones we need to investigate closer.
Let’s look for these composables in app_release-composables.txt
, starting with the AlbumScreen
function.
As you can see, the problem is in the unstable screenState: PlayerScreenState
. We can make this parameter stable to make the function skippable. Let us find it in app_release-classes.txt
All members of the class are stable, the compiler only shows <runtime stability> = Unstable
. Here is what the docs say about this:
Runtime means that stability depends on other dependencies which will be resolved at runtime (a type parameter or a type in an external module) … The line at the bottom indicates the “expression” that is used to resolve this stability at runtime.
So we need to try and make the class itself stable - let’s mark the class with the @Stable
annotation:
Next, we clear the cache and build again to see how the result has changed. We again open the app_release-composables.csv
table, and find restartable but not skippable methods. After our changes the number of such functions has decreased, so the @Stable
annotation helped.
Next, we'll look at other composables. One main problem stands out: Collections.
Let's deal with the Collections we use (Lists, Sets, etc.). The compiler can't determine when the list is immutable - List (as well as Set and other standard collection classes) are defined as interfaces in Kotlin, which means that the underlying implementation may still be mutable. For example, you could write:
val set: List<String> = mutableListOf(“foo”)
In this case the variable is constant, its declared type is not mutable but its implementation is still mutable. The Compose compiler cannot be sure of the immutability of this class as it only has access to the declared type that states that this class is unstable. Let’s look into how we can change this.
- Make a custom wrapper for the collection, annotating it with
@Immutable
. - Using the kotlin.collections.immutable library.
Both of these methods are applicable, but we chose the second one because it is a ready-made solution, although it's still in alpha at the time of writing. So, let’s implement the dependency:
implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable:x.x.x"
After we add stable lists where necessary:
In addition to List
, there is a problem with the SectionSelector
function that renders tabs:
Let's take a closer look at its implementation:
We see that the function is just a wrapper that contains a list and the current tab selection logic. We can get rid of this function by moving these elements into the next function in the hierarchy and therefore get rid of the non-skippable function. Let's gather the compiler statistics again. Now, there aren’t any restartable but skippable composables, which is great!
Dynamic expressions
Another thing we need to pay attention to is default parameter expressions that are @dynamic
.
The documentation says:
Default expressions should be
@static
in every case except for the following two cases:
- You are explicitly reading an observable dynamic variable. Composition Locals and state variables are an important example of this. In these cases, you need to rely on the fact that the default expression will be re-executed when the value changes.
- You are explicitly calling a composable function, such as
remember
. The most common use case for this is state hoisting.
The most common way you’ll encounter the first case is MaterialTheme as default on your composables. Here's an example - marking the color parameter @dynamic
is normal in this case.
The second case is the use of remember
. In our case, we remember
the state of the scroll:
In all other cases where the default parameter is marked with @dynamic
, you should try to get rid of the default values or use the @Stable
annotation. For example, in the NowPlayingAlbumScreen
function the parameter insets
is marked as @dynamic
.
Let's annotate it with @Stable
to show the compiler that this parameter is stable by default.
Another case in this project was specifying a default parameter to recompose a function as a unit. In this case we just get rid of the default parameter.
Yet another case occurred when a default parameter was specified as a different default parameter from the same function.
Conclusion
As a result of all this work, all the restartable functions are skippable, all classes in the report file are stable, and we got rid of default parameter expressions that are @dynamic
when not needed. The profiler shows no more jank frames when running the app.
Here is a before and after graph of the time it takes to draw each frame. The improved implementation never goes above the hard cut-off time for rendering a frame, with a healthy margin left.
Before:
After:
This article closes the “Replicating dribbble in Jetpack Compose” miniseries (Part 1, Part 2, Part 3, Part 4). As you can see, using Jetpack Compose you can make complex screens and animations using a declarative approach. It might be different from what you’re used to with AndroidView, but it’s a powerful tool nonetheless and will only become more and more widespread in the future.
The repo contains the full implementation. Make sure to check our blog for more Jetpack Compose and iOS SwiftUI tutorials and open-source libraries!