Compose Guard: Detecting Regressions In Jetpack Compose
As Jetpack Compose becomes more widely used across Android (and Multiplatform projects!), detecting regressions via tooling becomes more important to shift left and detect regressions earlier in the development cycle instead of relying on manually catching errors.
These types of regressions are helpful to catch for a multitude of reasons. More context can be found here, but below are a few high level reasons:
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
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
Not all classes need to be stable, but a class being stable unlocks a lot of flexibility for the compose compiler to make optimizations when a stable type is being used in places, which is why it is such an important concept for compose.
To help detect regressions via tooling, you can utilize a new Gradle Plugin called Compose Guard. Compose Guard allows you to generate compose compiler metrics and detect regressions to those metrics on PRs or release builds on CI.
Compose Guard currently checks for numerous regressions that would be useful to a team:
New restartable but not skippable
@Composables
are addedNew unstable classes are added (only triggers if they are used as a
@Composable
parameter)New
@dynamic
properties are added to a@Composable
New unstable parameters are added to a
@Composable
By being able catch these regressions to the UI layer in PRs, you can ensure better performance without developers having to manually catch these types of issues in a PR review.
Adding Compose Guard To Your Project
Compose Guard is available via Maven Central. To start, add the plugin to your root build.gradle.kts
file.
plugins {
id("com.joetr.compose.guard") version "<latest version>" apply false
}
Then, you can apply the plugin to any subsequent modules that contain your Compose code. This can be simplified by utilizing convention plugins as well. I will use Now In Android as an example:
AndroidApplicationComposeConventionPlugin.kt
import com.android.build.api.dsl.ApplicationExtension
import com.google.samples.apps.nowinandroid.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.getByType
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.application")
// apply Compose Guard Plugin
pluginManager.apply("com.joetr.compose.guard")
val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension)
}
}
}
AndroidLibraryComposeConventionPlugin.kt
import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.kotlin
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
// apply Compose Guard Plugin
pluginManager.apply("com.joetr.compose.guard")
val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(extension)
}
}
}
Now that you've applied the plugin, you will notice new Gradle tasks appear for the project. Compose Guard adds two new tasks for each variant as well as one 'clean' task.
<variant>ComposeCompilerGenerate
(examplereleaseComposeCompilerGenerate
)- Generate golden compose metrics to compare against
<variant>ComposeCompilerCheck
(examplereleaseComposeCompilerCheck
)- Generates new metrics and compares against golden values
composeCompilerClean
- Deletes all golden and check compiler metrics
You can execute a root level prodReleaseComposeCompilerGenerate
task to generate the metrics for every module in Now In Android (but you can also just run :app:prodReleaseComposeCompilerGenerate
to generate the the metrics for just the :app
module.
After running the above task, you should now see metrics generated for every module under the compose_reports
folder of that module.
The folder name can be customized via Kotlin DSL in the module's build file.
composeGuard {
outputDirectory = layout.projectDirectory.dir("custom_dir").asFile
}
Now that the golden metrics are generated, let's add an unstable class to a @Composable
! I picked a random @Composable
in the NIA app to demo this feature.
data class NewUnstableClass(var unstableParameter: Int)
@Composable
private fun LoadingState(modifier: Modifier = Modifier, unstableParameter: NewUnstableClass) {
NiaLoadingWheel(
modifier = modifier
.fillMaxWidth()
.wrapContentSize()
.testTag("forYou:loading"),
contentDesc = stringResource(id = R.string.feature_bookmarks_loading),
)
}
Now, if I run ./gradlew prodReleaseComposeCompilerCheck
, you should see the build fail!
* What went wrong:
Execution failed for task ':feature:bookmarks:prodReleaseComposeCompilerCheck'.
> New unstable classes were added!
ClassDetail(className=NewUnstableClass, stability=UNSTABLE, runtimeStability=UNSTABLE, fields=[Field(status=stable, details=var unstableParameter: Int)], rawContent=RawContent(content=unstable class NewUnstableClass {
stable var unstableParameter: Int
<runtime stability> = Unstable
}))
More info: https://github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/compiler-metrics.md#classes-that-are-unstable
By adding this check to PR and release build jobs on CI, you can start to detect these types of UI regressions!
Summary
By adding Compose Guard to your project, you can start to detect UI regressions via tooling. These UI regressions have a multitude of performance implications for a project, so it's important to try and catch issues as early as possible.
Notice
I, Joseph Roskopf, am one of the authors of this Gradle Plugin along with Joshua Swigut.
Subscribe to my newsletter
Read articles from Joe Roskopf directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Joe Roskopf
Joe Roskopf
Hey! I’m Joe! I’m an Android developer who is passionate about the human side of software development.