Debugging Jetpack Compose (Based on true story! 🔪🩸)

Kaaveh Mohamedi
ProAndroidDev
Published in
6 min readSep 28, 2023

--

Some days ago, I started seeking performance issues in ComposeNews, especially unnecessary recompositions. This is my journey into a mystery land!

Compose compiler; behind the scene!

Teaser

In the first step, I tried to apply best practices and correct issues mentioned in my previous article. I was almost certain there weren’t any more issues until I looked at the Layout inspector! 😨 There was horrible things happening under the hood! 😰

134 extra recomposition happed!

Act I: A Taste of Solitude

For dipper investigation, I remembered Chris Bane’s article named Composable metrics. So, I started setting up Compose compiler report in the project:

  1. First, config the Compose metrics into the root build gradle file:

Next, press ctrl twice to open the Run Anything window. it just needed to execute this gradle task:

gradle assembleRelease -PcomposeCompilerReports=true

After that, the reports are saved into the build folder of every project module.

I opened marketlist_release-composables.txt :

MarketListScreen and MarketListItem weren’t skippable! 😱

But wait! What does skippable mean? According to Compose compiler document:

Skippability means that when called during recomposition, compose is able to skip the function if all of the parameters are equal.

In other words, when composable function parameters are equal to their previous values, the Compose compiler skips the recomposition of this composable function. It’s logical, right? When there is nothing changes, there is no reason to execute the function again! But, we don’t need to know about Restartability for now. Let’s just focus on Skippability.

When doe s a Composable function become Skippability and when not? I saw closer and figured out the marketListState was unstable! 🤦🏻‍♂️

In the next step, we need to know what stability means and why it’s important for the Skippability of functions.

- In a simple word, if all parameters of a function are stable, the function is marked skippable.

- But MarketListRoute is marked skippable even though the viewModel isn’t stable!

- I know! If you look closely, it has a default value marked @dynamic. It’s not important at this moment.

So, what types are considered stable? According to docs:

  • All immutable objects such as all primitive types and Strings. Also, data class with all val property (types that never change after the be constructed)
  • All mutable objects that the Compose compiles will be notified somehow when the value is changed. Such as MutableState objects returned by mutableStateOf()

There is an interesting situation. Collection classes like List, Set and Map are always determined unstable. But why? let’s see an example:

val contactList = mutableListOf(
Contact(...)
...
)

@Composable
fun ContactList(contactList: List<Contact>){
...
}

As you noticed, one of the implementations of the interface of List is MutableList. So, the contactList maybe updated (any item is deleted or added a new one) outside of the composable function and there is no way to tell the compiler the parameter changed (This is why we wrap it with State). For forcing immutability to the Kotlin Collections, There are the Kotlinx immutable collections that can be used.

In contrast, all types that are not mentioned above, are considered unstable. For example, any classes come from third-party libraries even your model classes come from non-compose modules (like data and domain layer modules).

Act II: The Dark Along the Ways

I dug deeper and checked marketlist_release-classes.txt :

Aha! The evidence toward me to two issues! First, the first variable’s type of State was List . I forgot to use immutable data structures like PersistList . Second, the Market itself was unstable! But why? I checked out the declaration:

Right! isFavorite define as var not val ! I corrected these issues and checked again the Compose compiler reports:

unstable class State {
unstable val marketList: PersistentList<Market>
stable val refreshing: Boolean
stable val showFavoriteList: Boolean
<runtime stability> = Unstable
}

unstable class OnFavoriteClick {
unstable val market: Market
<runtime stability> = Unstable
}

....

They were still unstable! 🤦🏻‍♂️

Act III: The White Tower

At that point, I remembered a tip:

Any classes come from third-party libraries even your model classes come from non-compose modules (like data and domain layer modules).

The Market model was in the domain layer that hadn’t Compose as dependency. So, that was why the Market was unstable! 🤔 I added the Compose runtime in dependencies to mark Market with @Immutable from androidx.compose.runtime.Immutable .

Domain layer’s build.gradle.kt

I checked the report again:

stable class State {
stable val marketList: PersistentList<Market>
stable val refreshing: Boolean
stable val showFavoriteList: Boolean
<runtime stability> =
}

stable class OnFavoriteClick {
stable val market: Market
<runtime stability> = Stable
}

....

Yay! It became stable! 🥳

restartable skippable ... fun MarketListScreen(
stable marketListState: State
stable onNavigateToDetailScreen: Function1<@[ParameterName(name = 'market')] Market, Unit>
stable onFavoriteClick: Function1<@[ParameterName(name = 'market')] Market, Unit>
stable onRefresh: Function0<Unit>
)

restartable skippable ... fun MarketListItem(
stable modifier: Modifier
stable market: Market
stable onItemClick: Function0<Unit>
stable onFavoriteClick: Function0<Unit>
)

....

And MarketListScreen and MarketListItem became skippable! 🤩

I went for the next feature module (MarketDetail) to check its status:

restartable skippable ... fun MarketDetailScreen(
marketDetailState: State
stable onFavoriteClick: Function1<@[ParameterName(name = 'market')] Market?, Unit>
)

The MarketDetailScreen been skippable but the marketDetailState hadn’t any stability marker! I looked at the stability of the State itself:

runtime class State {
stable val market: Market?
stable val loading: Boolean
stable val refreshing: Boolean
runtime val marketChart: Chart
<runtime stability> = Runtime(Chart)
}

Yeap! it was because of the Chart. But what is runtime means? According to the doc:

Runtime means that stability depends on other dependencies which will be resolved at runtime (a type parameter or a type in an external module).

I felt a bad smell! I checked the declaration of the Chart Model:

data class Chart(
val prices: List<Pair<Int, Double>>,
)

The type of prices was List and it was defined in another module (domain module). If you remember, I set Compose compiler in that module, It just needed to replace List with PersistList.

stable class Chart {
stable val prices: PersistentList<Pair<Int, Double>>
<runtime stability> =
}

The Chart became stable. ☺️

Act IV: The Salvation

Maybe you noticed that the domain layer became dependent on Jetpack Compose. This is a bad smell! The business layer is dependent on a library for UI things! It’s time to refactor the code to become cleaner. For this reason, I defined a model class for Market in the features module and removed all compose stuff from the domain layer.

In the next step, I refactored all composables to depend on MarketModel instead of Market. I checked the Compose compiler reports and everything was fine as before. 🤩

If you want to see the final code, check this repo:

Post-credits scene

This adventure helped me to know the Jetpack Compose better. After that, the Compose became more elegant and joyful to me! ☺️

Compose compiler; when you know her deeper!

For the last tip I think, in the Compose compiler’s next releases, there will be fewer such complexities. For example, as Leland Richardson, one of the software engineers working on the Compose compiler said:

It is my job to prevent the developers from needing to struggle with parameters stability.

But if you want to know even deeper, feel free and check out Leland’s videos:

--

--