Diagnose stability issues

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.

Recomposition and skips counts in the Layout Inspector

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.