Blog Infos
Author
Published
Topics
Published

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:
plugins {
libs.plugins.apply {
alias(android.application) apply false
alias(kotlin.parcelize) apply false
alias(android.library) apply false
alias(kotlin.android) apply false
alias(hilt.android) apply false
alias(kotliner) apply false
alias(detekt) apply false
alias(ksp) apply false
}
}
// Compose metric configuration 👇🏻👇🏻👇🏻
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler"
)
}
if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.absolutePath}/compose_compiler"
)
}
}
}
}
// Compose metric configuration 👆🏻👆🏻👆🏻
view raw build.gradle.kt hosted with ❤ by GitHub

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 ListSet and Map are always determined unstable. But why? let’s see an example:

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

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:

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Market(
val id: String,
val name: String,
val currentPrice: Double,
val imageUrl: String,
var isFavorite: Boolean = false,
): Parcelable
view raw Market.kt hosted with ❤ by GitHub

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.

import ir.composenews.domain.model.Market
data class MarketModel(
val id: String,
val name: String,
val currentPrice: Double,
val imageUrl: String,
val isFavorite: Boolean = false,
)
fun MarketModel.toMarket() = Market(
id = id,
name = name,
currentPrice = currentPrice,
imageUrl = imageUrl,
isFavorite = isFavorite,
)
fun Market.toMarketModel() = MarketModel(
id = id,
name = name,
currentPrice = currentPrice,
imageUrl = imageUrl,
isFavorite = isFavorite,
)
view raw MarketModel.kt hosted with ❤ by GitHub

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

, 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:

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

 

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu