If you are experiencing performance issues that result from unnecessary or
excessive recomposition, you should debug the stability of your app. This guide
outlines several methods for doing so.
Layout Inspector
The Layout Inspector in Android Studio lets you see which composables are
recomposing in your app. It displays counts of how many times Compose has
recomposed or skipped a component.
Compose compiler reports
The Compose compiler can output the results of its stability inference for
inspection. Using this output, you can determine which of your composables are
skippable, and which are not. The follow subsections summarize how to use these
reports, but for more detailed information see the
technical
documentation
.
Setup
Compiler compiler reports are not enabled by default. You can activate them with
a compiler flag. The
exact setup
varies depending on your
project, but for most projects you can paste the following script into your root
build.gradle
file.
Groovy
subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_compiler"
]
}
if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_compiler"
]
}
}
}
}
Kotlin
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"
)
}
}
}
}
Run the task
To debug the stability of your composables, run the task as follows:
./gradlew assembleRelease -PcomposeCompilerReports=true
Example output
This task outputs three files. The following are example outputs from
JetSnack
.
<modulename>-classes.txt
:
A report on the stability of classes in this
module.
Sample
.
<modulename>-composables.txt
:
A report on how restartable and
skippable the composables are in the module.
Sample
.
<modulename>-composables.csv
:
A
CSV
version of the composables report
that you can import into a spreadsheet or processing using a script.
Sample
Composables report
The
composables.txt
file details each composable functions for the given
module, including the stability of their parameters, and whether they are
restartable or skippable. The following is a hypothetical example from
JetSnack
:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun SnackCollection(
stable snackCollection: SnackCollection
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
stable index: Int = @static 0
stable highlight: Boolean = @static true
)
This
SnackCollection
composable is completely restartable, skippable and
stable. This is generally preferable, although certainly not mandatory.
On the other hand, let's take a look at another example.
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
The
HighlightedSnacks
composable is not skippable. Compose never skips it
during recomposition. This occurs even if none of its parameters have changed.
The reason for this is the
unstable
parameter,
snacks
.
Classes report
The file
classes.txt
contains a similar report on the classes in the given
module. The following snippet is the output for the class
Snack
:
unstable class Snack {
stable val id: Long
stable val name: String
stable val imageUrl: String
stable val price: Long
stable val tagline: String
unstable val tags: Set<String>
<runtime stability> = Unstable
}
For reference, the following is the definition of
Snack
:
data class Snack(
val id: Long,
val name: String,
val imageUrl: String,
val price: Long,
val tagline: String = "",
val tags: Set<String> = emptySet()
)
The Compose compiler has marked
Snack
as unstable. This is because the type of
the
tags
parameter is
Set<String>
. This is an immutable type, given that it
is not a
MutableSet
. However, standard collection classes such as
Set, List
,
and
Map
are ultimately interfaces. As such, the underlying implementation may
still be mutable.
For example, you could write
val set: Set<String> = mutableSetOf("foo")
. The
variable is constant and 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 sees the declared type. It therefore marks
tags
as unstable.