◉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.

Android27Jetpack Compose6RecompositionPerformance6Composition 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 $15 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-05-25
One Month Splitting Antigravity's Inline Edit and Agent Mode Across Four Wallpaper Apps
A month of notes from running Antigravity's Inline Edit and Agent Mode across four production wallpaper apps — with real counts, the decision rule I wrote into AGENTS.md, a one-pass dSYM fix, and how credit cost factors in.
▣ App Dev2026-10-08
Three Safety Nets to Test with a Fake agy Before Running the Antigravity CLI in a Container
Before you call the Antigravity CLI (agy) from Docker or a Cloud Run job, reproduce three failure modes with a fake agy: an installer that returns gzip, a hang on tool approval, and an exit code of 0 with nothing produced. Everything here rests on official docs and on code I ran myself.
▣ App Dev2026-09-29
Purging Cloudflare Didn't Bring Back the New Wallpapers: the Stale JPEGs Were Held by XPageSpeed on the Origin
An operations record from a wallpaper app: images overwritten at the same URL kept arriving stale even after a Cloudflare purge. Covers the three-way local/origin/edge probe that names the guilty layer, when prefix purge beats per-URL purge, and how to switch off Xserver's XPageSpeed in .htaccess without a 500.
📚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