ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-25Advanced

Every Favorite Tap Was Redrawing Every Visible Wallpaper — Stopping Needless Recomposition in Compose, Measured

A record of fixing a wallpaper grid where toggling a single favorite recomposed every visible thumbnail. Covers turning on Composition Tracing to measure first, tracking the unstable parameter down, stabilizing the data model, handling lambdas and derived state, and comparing recomposition counts before and after from an indie developer's view.

Android26Jetpack Compose5RecompositionPerformance5Composition Tracing

Premium Article

While reworking the favorites screen of one of my wallpaper apps, I noticed that every time I tapped the heart on a thumbnail, the dozen-or-so thumbnails on screen flickered for a frame. Only the tapped tile should change, yet every visible cell in the viewport looked like it was being redrawn.

During a scroll, that flicker turns into a stutter. On my own Pixel it stayed barely smooth, but on the older phone a friend lends me it clearly hitched. When you maintain the same app for years as an indie developer, you get used to the speed of your own device and stop noticing this kind of regression. I caught it while scrolling and mashing the favorite button on a slow device. The cause was that Jetpack Compose's recomposition scope had quietly grown too wide, so I started by turning "how many times is this redrawing?" into a number.

Don't start tuning on a guess — make recomposition counts visible

The worst thing you can do with a Compose performance problem is sprinkle remember and key around on intuition. If you change things without knowing which Composable recomposes how often, you can't tell whether it helped, and you break something else in the process.

The first step was enabling Composition Tracing. It makes the System Trace show, by name, which Composable functions composed and recomposed and how many times. The setup itself is boilerplate, so I asked the Antigravity agent to "enable composition tracing for debug builds only" and reviewed the diff it produced.

// app/build.gradle.kts
dependencies {
    // For Composition Tracing (debug is enough)
    implementation("androidx.tracing:tracing-perfetto:1.0.0")
    implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
}
 
// Tell the Compose compiler to emit composition trace markers
composeCompiler {
    // Only true for debug measurement. Never ship this in release.
    includeTraceMarkers = true
}

I captured the trace with the System Trace profiler in Android Studio, or by launching androidx.tracing.perfetto from Macrobenchmark. After launch I toggled one favorite, opened the trace in Perfetto, and counted the slices for the thumbnail Composable. That was the moment "one toggle drives recompositions equal to the visible cell count" stopped being a suspicion and became a fact.

The culprit was an unstable parameter

The classic reason recomposition spreads is that an argument passed to a Composable is judged "unstable" by the Compose compiler. Any Composable that takes even one unstable argument drops out of being skippable, and gets recomposed unconditionally whenever its parent recomposes.

My grid item was receiving this data class.

// Looks like an ordinary model, but Compose sees it as unstable
data class WallpaperItem(
    val id: String,
    val thumbnailUrl: String,
    val tags: List<String>,        // List is an interface, treated as unstable
    var isFavorite: Boolean,       // var is the clincher for instability
)

List<String> is an interface type whose implementation could be swapped, so the compiler can't assume its contents won't change. And a var property is the clincher, because there's no guarantee about when it gets reassigned. Because this one model was unstable, the whole thumbnail Composable that received it stopped being skippable, and every time the favorites Set updated and the parent recomposed, all the visible cells were dragged along.

You can confirm the stability verdict in the Compose compiler's report output.

# With compiler reports configured in build.gradle.kts
./gradlew assembleRelease \
  -Pandroidx.compose.compiler.reports.destination=build/compose_reports
 
# The generated *-classes.txt lists stable/unstable per class
# e.g. unstable class WallpaperItem

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Wire up Composition Tracing so you can read how many times each Composable recomposes per interaction as a number, not a guess
Trace why a LazyVerticalGrid redrew every visible thumbnail on each favorite toggle down to the unstable parameter, then narrow recomposition to the single tapped cell with @Immutable, stable keys, and derivedStateOf
Compare recomposition counts and scroll frame times before and after, and find a line for how much measurement to delegate to an agent versus which design decisions to keep yourself
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-07-06
Building a Live Wallpaper That Doesn't Drain the Battery — Designing the WallpaperService Render Loop Around a Power Budget
Live wallpapers quietly burn battery because they keep drawing when nothing is visible. Here is the WallpaperService.Engine design I used to cut power draw by roughly 60% in a real wallpaper app, built around visibility, idle, and thermal gates.
App Dev2026-06-26
Green in the Embedded Emulator, Broken on the First Real Device — Putting a Parity Gate on AI-Generated Compose Apps
AI Studio now generates Kotlin/Compose apps from a prompt, runs them in an embedded emulator, and pushes them to a real device over USB — all from one screen. Yet a screen that passed in the emulator can break the first time it lands on a real phone. As a solo developer running several apps, here is how I put a gate that catches device parity issues before they ship.
App Dev2026-06-25
The Day Rolled Over and Yesterday's Wallpaper Came Back — Designing Date Boundaries Instead of Trusting an Agent's Time Code
A record of redesigning a daily-rotating wallpaper feature that showed yesterday's pick after midnight for some users and switched twice in one night for travelers. I separate the device clock, time zone, and daylight saving time into three distinct failure sources, show how to define a stable day key, defend reward boundaries against a rewound clock, and test it all with an injectable clock — from a solo indie developer's point of view.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →