Blog Infos
Author
Published
Topics
, , ,
Published

Unsplash@niko_photos

etpack Compose revolutionized Android UI development with its declarative paradigm, but this simplicity comes with a hidden complexity: understanding recomposition behavior. When a composable recomposes unnecessarily, you pay a performance cost, CPU cycles spent rerendering UI that didn’t actually change. The challenge isn’t just identifying these issues, it’s understanding why they happen in the first place.

The Compose compiler has always performed stability analysis behind the scenes, determining which composables can skip recomposition when their inputs haven’t changed. But this analysis happens silently at compile time, leaving developers in the dark about whether their composables are optimized or not. You might write what looks like perfectly stable code, only to discover later that a single var property in a class makes everything unstable, cascading through your entire UI tree.

Compose Stability Analyzer brings this hidden analysis into the light. It’s a comprehensive toolset that provides real-time visual feedback in your IDE, runtime recomposition tracing with annotations, and stability validation in your CI pipeline. Instead of waiting for performance problems to manifest in production, you get immediate feedback while you code, making stability analysis a natural part of your development workflow.

In this article, you’ll explore how Compose Stability Analyzer works, examining the IntelliJ plugin that brings visual stability indicators to your IDE, the compiler plugin that enables runtime recomposition tracing, and the stability validation system that prevents regressions from reaching production.

Understanding the stability problem: Why composables recompose

Before diving into the tools, it’s worth understanding what the Compose compiler actually analyzes. The compiler performs stability inference for every composable function, answering two questions:

1. Is this composable skippable?
A composable is skippable when the compiler can prove that recomposing it with the same inputs will produce the same result. If all parameters are stable and haven’t changed, the composable can skip recomposition entirely , a significant performance optimization.

2. Is this composable restartable?
A composable is restartable when it can be recomposed independently without recomposing its parent. This enables more fine-grained recomposition. Most composables are restartable, but those marked with certain modifiers might not be.

The important concept here is parameter stability. A parameter is stable when the Compose compiler can reliably detect whether its value has changed. There are such complicated rules, and if you really want to deep dive for a better understanding of them, you can refer to the research on stability, Compose Stability Inference.

Visual stability analysis in your Android Studio

The Compose Stability Analyzer IntelliJ Plugin brings stability analysis directly into Android Studio and IntelliJ IDEA, providing four layers of visual feedback as you write code.

Installation and setup

Installing the plugin is straightforward:

  1. Open Android Studio → Settings → Plugins → Marketplace.
  2. Search for “Compose Stability Analyzer”.
  3. Click Install and restart your IDE.

Once installed, the plugin immediately starts analyzing your code. You don’t need to configure anything , it works out of the box with any Compose project.

Gutter icons: Instant visual feedback

The most immediately visible feature is gutter icons, colored dots that appear in the left margin next to composable functions:

  • Green dot: The composable is skippable. All parameters are stable, and the compiler can optimize recomposition.
  • Yellow dot: The composable is restartable but not skippable. It will recompose when its parent recomposes.
  • Red dot: The composable has unstable parameters that prevent optimization.

This instant visual feedback is the fastest way to spot performance issues in Jetpack Compose. A quick scan down the left margin shows you which composables need attention. If you see a screen full of red dots, you know there’s work to do.

The gutter icons are computed based on the same stability analysis that the Compose compiler performs. The plugin hooks into the compiler’s analysis phase and extracts the stability information, then renders it visually in the editor.

But you should keep in mind that, now, the Strong skipping mode is mostly enabled by default, so all your composable functions will be skippable by default. You can enable or disable the strong skipping mode check on the plugin configurations (Tools > Compose Stability Analyzer), and it’s also enabled by default since version 0.5.0. But if you want to give more attention for your stability problems, you can just check it off depending on your situation.

Hover tooltips: Understanding the why

Gutter icons tell you what is wrong, but hover tooltips tell you why. When you hover your mouse over a composable function name, a detailed tooltip appears:

The tooltip shows:

  • Whether the composable is skippable and restartable
  • The total count of stable vs. unstable parameters
  • Detailed stability information for each parameter
  • Receiver stability (for extension functions)

This granular feedback is invaluable for debugging. Instead of guessing which parameter is causing instability, you see exactly which ones are problematic.

Inline parameter hints: Granular visibility

While tooltips provide detailed information on hover, inline parameter hints give you continuous visibility without any interaction:

Small badges appear right next to each parameter’s type in the function signature, showing whether that specific parameter is stable or unstable. This is the most detailed level of feedback; every parameter’s stability is visible at a glance.

For functions with many parameters, this helps you quickly scan and identify the problematic ones without opening tooltips. The color coding matches the stability state:

  • Green badge: Stable parameter
  • Red badge: Unstable parameter
  • Yellow badge: The stability will be decided at runtime (so it might be stable or not)
Code inspections: Automated suggestions

The fourth layer of feedback is active code inspections. When the plugin detects an unstable composable, it can:

  1. Highlight the issue with a warning underline.
  2. Suggest quick fixes via the Alt+Enter menu.
  3. Offer to add annotations like @TraceRecomposition for debugging.
  4. Provide suppression options if the instability is intentional.

This is where the plugin becomes proactive. Instead of just showing you problems, it suggests solutions. Press Alt+Enter on an unstable composable, and you might see options like:

  • Add @TraceRecomposition to monitor recompositions.
  • Mark with @Stable annotation (use with caution).
  • Suppress stability warning for this function.
Stability Explorer: Package-level analysis

Beyond individual function analysis, the Stability Explorer provides a bird’s-eye view of your entire codebase’s stability:

To enable it:

  1. Install the Compose Stability Analyzer Gradle plugin (covered in the next section).
  2. Go to View → Tool Windows → Compose Stability Analyzer.
  3. Build your project and click the refresh button.

The explorer shows a tree view of your package structure, with each composable function annotated with its stability status. You can quickly drill down to find unstable functions across your entire codebase, making it easy to prioritize optimization work.

This is particularly useful in large projects where manually reviewing every composable would be impractical. The explorer gives you a stability “report card” for your app, showing which packages have the most stability issues.

Custom Configurations

The plugin is highly customizable. You can adjust colors, enable or disable specific visual indicators, and configure analysis behavior to match your preferences.

Go to Settings → Tools → Compose Stability Analyzer to access configuration options:

You can:

  • Change gutter icon colors to match your IDE theme
  • Enable or disable inline hints, warnings, and gutter icons independently
  • Configure Strong Skipping mode analysis
  • Add ignored type patterns to exclude certain classes from analysis
  • Enable analysis in test source sets
  • Set a custom stability configuration file

This flexibility ensures the plugin fits naturally into your existing workflow, whether you prefer minimal visual noise or maximum information density.

The Gradle plugin: Runtime recomposition tracing

While the IntelliJ plugin provides compile-time visibility, the Gradle plugin enables runtime analysis, tracking exactly when and why your composables recompose in a running app. This is achieved through the @TraceRecomposition annotation, a compiler plugin that instruments your composables with logging code.

Installation and setup

Add the dependency below to your libs.versions.toml file:

stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.5.0" }
view raw csa-dep.kt hosted with ❤ by GitHub

Next, apply the plugin on your root’s build.gradle.kts file like below:

alias(libs.plugins.stability.analyzer)

That’s it. The plugin automatically applies the compiler plugin to all Kotlin compilation tasks in your project. No additional configuration is required for basic usage.

Kotlin version compatibility

The plugin is tightly coupled to the Kotlin compiler, so version alignment is critical. At the moment of writing this article, you should use Kotlin 2.2.21 at least. Using mismatched versions may lead to compilation errors. Always use the exact Kotlin version that matches your Stability Analyzer version. You can check out the Kotlin Version Mapping for the references.

The @TraceRecomposition annotation

Once the plugin is installed, you can annotate any composable with @TraceRecomposition to track its recomposition behavior:

@TraceRecomposition
@Composable
fun ProductCard(
product: Product,
onClick: () -> Unit
) {
Card(onClick = onClick) {
Text(product.name)
Text("$${product.price}")
}
}

When this composable recomposes, you’ll see detailed logs in Logcat:

D/Recomposition: [Recomposition #1] ProductCard
D/Recomposition: ├─ product: Product stable (Product@abc123)
D/Recomposition: └─ onClick: () -> Unit stable (Function@xyz789)
D/Recomposition: [Recomposition #2] ProductCard
D/Recomposition: ├─ product: Product changed (Product@abc123 → Product@def456)
D/Recomposition: └─ onClick: () -> Unit stable (Function@xyz789)

 

The logs show:

  • Recomposition count: How many times this instance has recomposed
  • Parameter stability: Whether each parameter is stable or unstable
  • Change detection: Which parameters changed, showing old and new values
  • Identity tracking: The hashcode of each parameter value

This granular information is invaluable for debugging. You can see exactly which parameter change triggered a recomposition, helping you understand your app’s recomposition patterns.

Filtering with the tag parameter

For large apps with many traced composables, logs can become overwhelming. The tag parameter helps you organize and filter them:

@TraceRecomposition(tag = "product-list")
@Composable
fun ProductCard(product: Product, onClick: () -> Unit) {
// ...
}
@TraceRecomposition(tag = "user-profile")
@Composable
fun ProfileHeader(user: User) {
// ...
}

Now logs include the tag:

D/Recomposition: [Recomposition #1] ProductCard (tag: product-list)
D/Recomposition: [Recomposition #1] ProfileHeader (tag: user-profile)

 

You can filter Logcat to show only specific tags:

  • Filter by tag: product-list to see only product-related recompositions.
  • Filter by tag: user-profile to see only user profile recompositions.

This is especially useful when combined with custom loggers (covered next), where you might want to send only certain tags to analytics services.

Setting a threshold to reduce noise

Most composables recompose 1–2 times during initial setup, these are expected and not performance issues. The threshold parameter filters out this noise:

@TraceRecomposition(threshold = 3)
@Composable
fun FrequentlyRecomposingScreen() {
// Logs will only appear after the 3rd recomposition
}

This focuses your attention on actual problems: composables that keep recomposing during user interaction, not just initial setup.

A practical use case: setting a high threshold and sending analytics events when it’s exceeded:

@TraceRecomposition(tag = "checkout", threshold = 10)
@Composable
fun CheckoutScreen() {
// If this recomposes 10+ times, something is wrong
}

Then in your custom logger (see next section), you can send a Firebase event when the threshold is exceeded, allowing you to monitor performance issues in production.

Configuring the logging system

By default, @TraceRecomposition doesn’t log anything, you need to enable logging in your Application class:

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Enable only in debug builds
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
}
}
view raw csa-init.kt hosted with ❤ by GitHub

This is pretty important, always wrap with BuildConfig.DEBUG to avoid potential security problem in production builds, if you don’t have any custom logger.

Custom loggers: Beyond Logcat
The default logger uses Logcat, but you can provide a custom implementation to send logs anywhere you want:

ComposeStabilityAnalyzer.setLogger(object : RecompositionLogger {
    override fun log(event: RecompositionEvent) {
        // Send to Firebase, Crashlytics, custom analytics, etc.
        if (event.recompositionCount >= 10) {
            FirebaseAnalytics.getInstance(this).logEvent("excessive_recomposition") {
                param("composable", event.composableName)
                param("count", event.recompositionCount)
                param("unstable_params", event.unstableParameters.joinToString())
            }
        }
    }
})

 

So even you can set the different logger depending on the debug or release mode. The RecompositionEvent contains:

  • composableName: The function name.
  • tag: The tag from @TraceRecomposition .
  • recompositionCount: How many times it’s recomposed.
  • parameterChanges: List of parameter changes with stability info.
  • unstableParameters: List of unstable parameter names.

This enables powerful monitoring patterns. You can:

  • Send analytics events for composables that recompose excessively
  • Track performance metrics in production (with appropriate privacy safeguards)
  • Build custom dashboards showing recomposition patterns
  • Alert on performance regressions in staging environments

A more real-world example that uses tags to filter what gets logged:

val tagsToLog = setOf("checkout", "product-list", "search")
ComposeStabilityAnalyzer.setLogger(object : RecompositionLogger {
override fun log(event: RecompositionEvent) {
if (BuildConfig.DEBUG) {
// In debug, log everything
Log.d("Recomposition", formatEvent(event))
} else {
// In release, only log tagged composables that exceed threshold
if (event.tag in tagsToLog && event.recompositionCount >= 5) {
// Send to analytics
FirebaseAnalytics.getInstance(this).logEvent("recomposition_issue", ...)
}
}
}
})
view raw customlogger.kt hosted with ❤ by GitHub
Understanding the logs

Let’s walk through how to interpret the logs. Consider this example:

D/Recomposition: [Recomposition #1] UserCard
D/Recomposition: ├─ user: User stable (User@abc123)
D/Recomposition: └─ onClick: () -> Unit stable (Function@xyz789)

 

What this means:

  • This is the first recomposition of this UserCard instance.
  • Both parameters are stable.
  • This is expected behavior for the initial composition.
D/Recomposition: [Recomposition #2] UserCard
D/Recomposition: ├─ user: User changed (User@abc123 → User@def456)
D/Recomposition: └─ onClick: () -> Unit stable (Function@xyz789)

 

What this means:

  • Second recomposition.
  • The user parameter changed (new instance).
  • onClick remained stable (same lambda instance).
  • This is normal, the parameter changed, so recomposition is expected.
D/Recomposition: [Recomposition #3] UserCard (tag: user-profile)
D/Recomposition: ├─ user: MutableUser unstable (MutableUser@xyz789)
D/Recomposition: └─ Unstable parameters: [user]

 

What this means:

  • Third recomposition.
  • The user parameter is unstable (likely has var properties).
  • This composable will recompose on every parent recomposition, even if user didn’t change.
  • Action required: Fix the MutableUser class to be immutable.
Stability validation: Preventing regressions in CI

The most powerful feature of Compose Stability Analyzer isn’t the visual feedback or runtime tracing , it’s the ability to prevent stability regressions from reaching production. The stability validation system acts like git diff for composable stability, failing your CI build if stability degrades.

The problem: Silent performance regressions

Imagine this scenario: You’ve spent weeks optimizing your app. Every composable is stable, skippable, and fast. Then a teammate innocently changes a data class:

// Before (stable)
data class User(val name: String, val age: Int)
// After (unstable)
data class User(var name: String, var age: Int)
view raw example.kt hosted with ❤ by GitHub

This single change cascades through your entire UI tree. Dozens of composables that were skippable become non-skippable. Performance regresses, but the code review doesn’t catch it, there’s no visible indication that anything went wrong.

Stability validation prevents this. It tracks your composables’ stability over time and automatically fails builds when stability regresses, forcing the issue to be addressed before merging.

How it works: Stability snapshots

The validation system works through two Gradle tasks:

  • stabilityDump: Creates a .stability file containing the stability state of all composables.
  • stabilityCheck: Compares the current code against the baseline and fails if stability regressed.

Think of it as:

  • stabilityDump = “Save the current state”
  • stabilityCheck = “Has anything changed since last save?”
Step 1: Create a baseline

After compiling your project, run:

./gradlew :app:compileDebugKotlin
./gradlew :app:stabilityDump

 

This generates app/stability/app.stability:

@Composable
public fun com.example.UserCard(user: com.example.User): kotlin.Unit
  skippable: true
  restartable: true
  params:
    - user: STABLE (immutable data class)

@Composable
public fun com.example.ProductList(items: kotlin.collections.List<com.example.Product>): kotlin.Unit
  skippable: true
  restartable: true
  params:
    - items: STABLE (immutable collection with stable elements)

 

This file is human-readable and shows exactly what the compiler determined about each composable. It’s a complete snapshot of your app’s stability state.

Commit this file to git:

git add app/stability/app.stability
git commit -m "Add stability baseline"
git push

 

Now everyone on your team has the same baseline.

Step 2: Check for regressions

In your CI pipeline, run:

./gradlew :app:compileDebugKotlin
./gradlew :app:stabilityCheck

 

If nothing changed, the task succeeds:

✅ Stability check passed.

 

If stability regressed, the task fails:

❌ Stability check failed!

 

The following composables have changed stability:

~ com.example.UserCard(user): parameter ‘user’ changed from STABLE to UNSTABLE

If these changes are intentional, run ‘./gradlew stabilityDump’ to update the baseline.

The build fails, preventing the pull request from being merged until the issue is fixed or the regression is explicitly accepted.

Types of changes detected

The validator detects four types of changes:

 

This comprehensive tracking ensures that any change to stability is visible and requires conscious decision-making.

CI/CD integration

Add stability validation to your GitHub Actions workflow:

name: Android CI

on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Build project
        run: ./gradlew :app:compileDebugKotlin
  stability_check:
    name: Compose Stability Check
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Check out code
        uses: actions/checkout@v5.0.0
      - name: Set up JDK
        uses: actions/setup-java@v5.0.0
        with:
          distribution: 'zulu'
          java-version: 21
      - name: Run stability check
        run: ./gradlew stabilityCheck

 

Now every pull request gets automatically checked. If stability regresses, the PR cannot merge.

Configuration options

You can customize what gets tracked:

// In your build.gradle.kts
composeStabilityAnalyzer {
stabilityValidation {
enabled.set(true)
outputDir.set(layout.projectDirectory.dir("stability"))
includeTests.set(false) // Exclude test code
// Ignore specific packages
ignoredPackages.set(listOf("com.example.internal"))
// Ignore specific classes (e.g., previews)
ignoredClasses.set(listOf("PreviewComposables"))
// Ignore entire modules
ignoredProjects.set(listOf("benchmarks", "examples"))
}
}
view raw settings.kt hosted with ❤ by GitHub

This is useful for excluding code that doesn’t need stability tracking, like preview composables or debug screens.

Excluding specific composables

Use @IgnoreStabilityReport to exclude individual composables from validation:

@IgnoreStabilityReport
@Preview
@Composable
fun UserCardPreview() {
UserCard(user = User("John", 30))
}
view raw ignore.kt hosted with ❤ by GitHub

Preview composables aren’t in production builds, so their stability doesn’t matter. Excluding them reduces noise in your stability reports.

Multi-module projects

For projects with multiple modules, each module gets its own .stability file:

project/
├── app/stability/app.stability
├── feature-auth/stability/feature-auth.stability
└── feature-profile/stability/feature-profile.stability

 

Run stabilityCheck at the root to check all modules:

./gradlew stabilityCheck

 

Or check individual modules:

./gradlew :feature-auth:stabilityCheck

 

Accepting intentional regressions

Sometimes stability regressions are intentional , you’re refactoring code and temporarily accepting lower stability. When this happens, update the baseline:

./gradlew :app:stabilityDump
git add app/stability/app.stability
git commit -m "Accept stability regression for UserCard (refactoring in progress)"
git push

 

This creates a documented decision in git history. The regression is no longer silent , it’s explicit and tracked.

Performance considerations and best practices

While the tools are powerful, there are performance characteristics and best practices to keep in mind. Some people have asked me whether they need to make every type stable, the answer is definitely NO.

Since the Strong skipping mode is out now each composable function is skippable, and it will optimize the stability performance already. Even though this plgin migth say, this is unstable or runtime stability, but you don’t really need to fix it for every composable function. Let’s see a simple example.

 

If you look into the Icon composable function from the compose-material3 library, this plugin says, it’s unstable since it contains runtime parameters. BUT, the painter parameter is mostly initialized with the painterResource function as like below:

val painter = painterResource(R.drawable.)
Icon(painter = painter, contentDescription = null)
view raw icon.kt hosted with ❤ by GitHub

If you look into the internal logic of painterResource, it’s essentially stable. Internally, it uses remember to cache the value in memory , so you can think of it as a small trade-off: “I’ll use a bit of memory to store this unstable instance, so I don’t have to trigger unnecessary recompositions.”

 

There’s also a common misconception about immutable collections — that simply using an ImmutableList instead of a regular List will always improve performance. That’s not always true.

Think about it this way: if you have a list with 1,000 heavy objects and you switch to an immutable list, your composable becomes skippable. But on each recomposition, Compose will now run equals() on every item in that list to check if the new instance matches the previous one. In some cases, that can actually hurt performance more than just re-rendering a lightweight UI that only displays, say, five of those items.

So, stability can have very different impacts depending on the situation, and trying to make your entire UI stable is often unnecessary, and useless. My hope is that this plugin doesn’t add to your fear about stability, instead, it should help you debug real performance issues more effectively and make smarter, data-driven decisions without overthinking stability.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Conclusion

In this article, you’ve explored the Compose Stability Analyzer, examining how it brings visibility to Compose’s hidden stability analysis through three complementary tools: the IntelliJ plugin for visual feedback during development, the Gradle plugin for runtime recomposition tracing, and the stability validation system for preventing regressions in CI.

The IntelliJ plugin transforms stability from an invisible compile-time concept to something tangible, gutter icons, tooltips, and inline hints that make stability issues immediately visible. The @TraceRecomposition annotation bridges the gap between compile-time analysis and runtime behavior, letting you see exactly when and why composables recompose. And the stability validation system acts as a safety net, ensuring that optimizations you’ve carefully crafted don’t silently regress as your codebase evolves.

Together, these tools make stability analysis a natural part of your development workflow. You don’t need to wait for performance problems to manifest, you see them immediately as you code. You don’t need to guess which parameter is causing recomposition — the logs tell you exactly what changed. And you don’t need to manually review every PR for stability regressions, the CI pipeline does it automatically.

Understanding stability is fundamental to writing performant Compose applications. With Compose Stability Analyzer, that understanding becomes effortless, visual, immediate, and enforced.

As always, happy coding!

— Jaewoong

This article was previously published on proandroiddev.com

Menu