Dispatch Issue #25

💌 In today's Dispatch: 👻 Snapchat has a surprise open-source hit 🔒 Bulletproof @Preview stripping 📖 Instagram's Compose playbook
Vinay Gaba Profile Image
Vinay Gaba on January 27, 2026
Hero Image

GM Friends. This is JetpackCompose.app's Dispatch. The #1 Doctor recommended source for your weekly Android knowledge.

This is Issue #25 and boy do we have a lot in store for you.

🥂 Tipsy Tip

Guarantee Your @Preview Functions Are Stripped from Release Builds

Here's a ProGuard trick that friend of the newsletter Alex Vanyo shared: you can enforce that @Preview functions get stripped from your release builds, not just hope they do.

Add this to your release-proguard-rules.pro

# Ensure all Previews have been stripped
-checkdiscard class * {
    @androidx.compose.ui.tooling.preview.Preview <methods>;
}
-keepclassmembers,allowshrinking class * {
    @androidx.compose.ui.tooling.preview.Preview <methods>;
}

The magic here is -checkdiscard. This rule tells R8 to fail the build if any @Preview methods survive into the final APK. The second rule with allowshrinking permits R8 to remove these methods during optimization.

Why does this matter? Preview functions often reference debug-only resources, mock data, or preview-specific parameters. If they accidentally survive into production, usually due to your release-only code referencing these preview functions, you're shipping dead code at best—and potential crashes at worst if those debug resources don't exist in release builds.

This is especially valuable for teams with strict APK size budgets or security requirements. You get a build-time guarantee rather than hoping your shrinking configuration is correct.

I strongly recommend that you go check your ProGuard configuration and if this checkdiscard line doesn't exist, seriously consider adding it. Easy way to earn some brownie points from your peers 😎

Check out Alex's implementation for the full context.


Spot the Bug

Android bug PTSD? Same. 🎧 bitdrift's Beyond the Noise podcast features war stories, hard lessons, and spicy takes from the engineers who've lived through crashes, ANRs, and "how did this ship?" moments at scale. Give it a listen.


🍾 Pop Quiz

Here's a reasonable looking snippet of code. What's wrong with it?

suspend fun fetchUserData(): Result<User> {
    return try {
        val response = api.getUser()
        Result.success(response)
    } catch (e: Exception) {
        Log.e("API", "Failed to fetch user", e)
        Result.failure(e)
    }
}

You will find the answer in a section below ⬇️

🤿 Deep Dive

How Instagram Actually Rolls Out Framework Migrations

Summer Kitahara from Instagram's Data and UI Architecture team appeared on the Meta Tech Podcast, and the level of detail she shared about their Compose migration is genuinely interesting. Most "how we adopted X at BigCo" talks stay surface-level. This one didn't.

The Three Adoption Types

Instagram doesn't treat all Compose adoption the same. They've defined three distinct categories, each with different risk profiles:

  1. Hybrid adoption — Compose components inside View-based screens. This is the trickiest because you're dealing with interop: a Compose button inside a RecyclerView item, or a Compose bottom sheet in a View-based activity. The two frameworks have fundamentally different rendering models, and making them play nice is non-trivial.
  2. Full-screen migrations — Rewriting an entire screen from Views to Compose in one shot. Only viable for simpler screens. If your screen has years of accumulated features and edge cases, this isn't realistic.
  3. New feature development — Building brand new screens in Compose from day one. Lowest risk, highest reward—no legacy baggage.

The Three Stages (This Is Where It Gets Interesting)

Each adoption type goes through three stages, and the first one is brilliant:

Stage 1: Experimentation — The infra team runs "learning tests" with zero intention of shipping. They'll take a real production screen, swap one component to Compose, ship it as an experiment, and study what breaks. They're measuring engagement and performance metrics, sure—but the real value is discovering unknown unknowns: tooling that crashes, logging that assumes Views exist, interop edge cases that aren't documented anywhere.

The insight here is that metrics are predictable. You can guess roughly how performance will be affected. What you can't predict is which random internal system will break because someone assumed view.getParent() would always return a ViewGroup.

Stage 2: Closed Adoption — Engineers who want to use Compose must submit a technical proposal to the infra team for review. The infra team checks: Does this screen have MVVM in place? (this was a hard requirement.) Are all the components they need available in Compose? Are there any weird edge cases like complex video players or custom animations that haven't been validated yet?

This is a gate, and it's intentional. They'd rather catch problems during proposal review than have a product team discover mid-sprint that their use case isn't supported.

Stage 3: Open/Recommended Adoption — Anyone can use Compose, regardless of experience level. In fact, at this stage, they recommend it over Views for the relevant adoption type.

The Hidden Costs Nobody Talks About

A few things Summer mentioned that don't make it into conference talks:

  • Framework fragmentation is painful. If your team owns a screen that now has Views, Compose, AND Litho (because someone experimented three years ago), you need engineers fluent in all three. Context-switching between XML layouts in the morning and Compose lambdas in the afternoon is genuinely exhausting.
  • Tooling assumptions break silently. Compose nodes aren't Views. This breaks: automated impression logging, UI hierarchy inspection, accessibility scanners, screenshot testing—basically anything that traverses the view tree. They've had to make entire categories of tooling "UI framework agnostic."
  • Maintenance doubles until you're done migrating. Design system tokens, shared components, testing utilities—everything needs parallel implementations. Instagram rebuilt their entire design system in Compose. And every future design change ships twice.

The Forcing Function I Love

Before any team can adopt Compose, they had a rule that they must have MVVM in place. Declarative UI requires unidirectional data flow—if your screen is a jungle of mutable state being modified from seventeen different callbacks, Compose will expose every sin. By requiring architectural cleanup first, they're getting two wins: cleaner architecture AND Compose readiness.

Now, I know a thing or two about adopting Jetpack Compose at scale 😉 (If you are new here — I led this effort for Airbnb). I certainly saw a lot of parallels with how this story played out even there. One big advantage we had was that since we were already big on unidirectional data flow and were using Epoxy and Mavericks in every screen, the sell was way easier since our architecture was already setup to leverage a framework like Compose. It was why we could start using it as early as 2021 and were among the first big teams to do so.

😆 Dev Delight

Special characters and emojis causing crashes Special characters and emojis have caused more crashes than null pointer exceptions….

Base case meme Note to self: verify base case first 😅

🤔 Interesting tid-bits

Snapchat Open-Sources Valdi: A Cross-Platform UI Framework

Snap just dropped something they've been using internally since the time I was still working there (I left Snap in 2019) — Valdi, a cross-platform UI framework that's been powering Snap's production apps for 8 years. Yes, eight years—they've been sitting on this while we've been debating Flutter vs. React Native vs. Compose Multiplatform.

The pitch? Write declarative TypeScript, compile to native views on iOS, Android, and macOS. No web views, no JavaScript bridges. The framework includes automatic view recycling, viewport-aware rendering (only visible views get inflated), and a C++ layout engine that runs on the main thread.

I've known about this for many years, however, I didn't see this coming at all — the project has raced to 16,000+ Github stars!!! I wasn't expecting this response, especially at a time when we have too many cross platform UI frameworks to choose from.

If you've tried it out, I'd love to learn more about what your experience has been like. Do you know any large scale companies that have already adopted it? I'd love to know (or I should just reach out to my colleagues and catch up — maybe I will!).

Don't Forget Your Annual Build Tools Spring Cleaning

Here's your friendly reminder that your development machine is probably hoarding gigabytes of build artifacts like a digital packrat. You can recoup significant disk space by purging some commonly forgotten folders.

The usual suspects:

  • ~/.gradle/caches/ — Gradle's cache grows unbounded. Run ./gradlew cleanBuildCache to clear just the build cache, or delete the entire folder if you need maximum space back (note: your next build will be slower as Gradle re-downloads dependencies)
  • ~/.android/avd/ — Old emulator images you forgot existed.
  • ~/.m2/repository/ — Maven's local repo can balloon over time.
  • Android Studio's system directories — Check ~/Library/Application Support/Google/AndroidStudio* (macOS) or ~/.config/Google/AndroidStudio* (Linux) for old IDE versions.

Pro tip: Before you delete anything, check how much space each folder is consuming. On macOS, du -sh ~/.gradle/caches/ will show you the damage. I've seen developers reclaim 50+ GB just from Gradle caches alone. Your SSD will thank you.

Why the Android Calculator Is More Accurate Than iOS

Ever noticed that Android's calculator handles edge cases better than iOS? There's actually a fascinating technical reason behind this. Android's calculator (designed by Hans-J. Boehm of C++ garbage collector fame) uses a fundamentally different approach than traditional IEEE 754 floating-point. It employs "constructive reals" — numbers represented as functions that can compute approximations to any requested precision on demand. Combined with rational number arithmetic for fractions and bignums for large integers, this means results are always accurate to the displayed precision, and you can scroll through as many digits as you want.

iOS's calculator uses more traditional fixed-precision arithmetic, which is why edge cases like (10^100 + 1 - 10^100) return 0 on iOS but correctly return 1 on Android. The difference isn't just presentation — it's a fundamentally different computational model.

iOS vs Android calculator comparison left: calculation on the iOS calculator | right: same calculation on the Android calculator

🍾 Pop Quiz: Solution

Spot the Bug Solution

This innocent-looking try-catch is swallowing CancellationException. When a coroutine is cancelled (user navigates away, ViewModel cleared, etc.), CancellationException is thrown to signal cancellation. By catching Exception, you're preventing the cancellation from propagating, breaking structured concurrency entirely.

The Fix:

catch (e: Exception) {
    e.rethrowIfCancellation()
    Log.e("API", "Failed to fetch user", e)
    Result.failure(e)
}

fun Throwable.rethrowIfCancellation() {
    if (this is CancellationException) throw this
}

🦄 How you can help?

If you enjoy reading this newsletter and would like to keep Dispatch free, here are two quick ways to help:

👉🏻 Chip in via Github Sponsors. If your company offers an education budget, contributing a "coffee" to keep this newsletter going will certainly qualify. You'll get an instant receipt for reimbursement, and I'll keep Dispatch running ☕️

👉🏻 Spread the word. Tweet, Bluesky, toot, Slack, or carrier‑pigeon this issue to someone who loves Android. Each new subscriber pushes Dispatch closer to sustainability.

On that note, here's hoping that your bugs are minor and your compilations are error free.

Welcome to the VIP club
To read the rest of the post, subscribe and never miss an issue!
Already a subscriber? Use the same account to login again.