JetpackCompose.app's Dispatch Issue #28: Android Dev Summit Special

💌 In today’s Dispatch: My panel discussion about agentic coding 🗣️ Compose state finally leaves the UI 🧬 Compose Previews escape Studio 🖼️ Cash App called this in 2021 🔮

Can your mobile release handle AI-generated code?

AI multiplied the amount of code your team writes. But the process that delivers it to users hasn't changed: same release trains, same quality gates, same manual handoffs. Join a live panel with mobile engineering leaders on what that volume changes about the release process.

📅 Nov 12 @ 10am PT/1pm ET

GM Friends. It’s me again after a bit of a break, and today it's an Android Dev Summit Special that I got the chance to attend in person.

This is Issue #28. My last issue shipped in May, but Google took four years between the Android Dev Summits, so I'm calling it a tie 😅

🤿 Deep Dive

Compose state just left the building: Jetpack AppState

ADS was mostly agents. But the announcement most likely to rewrite your repo got three minutes of keynote (at 20:00).

In May, Google made Android "Compose First" for UI. At Android Dev Summit, it made Compose First an architecture call with the introduction of App State and it’s two libraries:

  • Transform runs a @Composable block with no UI and returns a State<R>. Whatever it reads (State, a Flow via collectAsState, a LaunchedEffect) is tracked, and it recomposes when those inputs change.

  • StateStore is a type-safe, keyed container for app-wide state, exposed as Compose State, with optional DataStore backing to survive process death.

The pitch: no more combine pyramids.

Here’s an example of the before vs after:

// Before: the flatMapLatest + stateIn ritual
val uiState: StateFlow<HomeUiState> = networkMonitor.isOnlineFlow
    .flatMapLatest { online ->
        (if (online) repo.allTracksFlow else repo.downloadedTracksFlow)
            .map { HomeUiState.Success(it, online) }
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeUiState.Loading)


// After: AppState Transform
val uiState: State<HomeUiState> = transform(HomeUiState.Loading, scope = viewModelScope) {
    val isOnline by networkMonitor.isOnline
    val tracks = (if (isOnline) repo.allTracksFlow else repo.downloadedTracksFlow)
        .collectAsState(initial = null).value ?: return@transform HomeUiState.Loading
    HomeUiState.Success(tracks, isOnline)
}

It's a plain if/else, it only subscribes to the branch that ran, and it has no five-flow ceiling like typed combine. I like it a lot. I also have opinions.

  1. Your ViewModel now exports Compose types. uiState is State, not StateFlow, and any module calling transform needs compose = true and the Compose compiler plugin, UI or not.

  2. You lose WhileSubscribed. Hallelujah!!! A viewModelScope transform stays live for the whole scope.

  3. It runs on Dispatchers.Default. Great for heavy derivations, bad for tests. Inject the scope and dispatcher from day one and use transformTest { } with advance() from transform-testing that they also shipped along with this library.

  4. 🤫 This one I’m going to cover in the PopQuiz section

🦅 Eagle eyed readers would’ve already spotted this: is this just like Cash App’s Molecule library with a Jetpack badge?

Short answer: Mostly!!! Cash App shipped UI-less composition in 2021. Google's twists are returning State instead of StateFlow, adding a listener for side-effect-only observers, bundling StateStore with DataStore, and publishing for wasm, watchOS and tvOS (release page). It's built for KMP from day one.

My honest advice: Port your ugliest combine ViewModel on a branch and write its transformTest. If it reads better, that's your 2027 answer for which migrations your agents are going to help you with.

Nothing crashed. The audio just stopped mid-routine. Your worst bugs never trip the crash reporter, so it has nothing to say about them.

Hello, blind spot. Goodbye, good user experience.

See how Hatch used bitdrift to remove sampling and cut time to root cause 30-50%.

Your team got excited about the newly launched AppState and they used it for a tiny feature: count how many photos the user has opened this session.

class GalleryViewModel : ViewModel() {
    // set every time a photo is tapped
    var openedPhoto by mutableStateOf<Photo?>(null) 
    val photosOpened: State<Int> = transform(
        initialValue = 0, 
        scope = viewModelScope
    ) { previous ->
        if (openedPhoto != null) previous + 1 else previous
    }
}

It looks right, and the "tap one photo" test passes. But QA reports that the counter doesn't go up when the same photo is tapped twice in a row, and that it undercounts when you tap through photos quickly. Why?

You will find the answer in a section below ⬇️

📣 Personal Update

I had the fortune of participating in a panel discussion about Agentic Coding at ADS with some stalwarts, many of who I like to call friends: Ben Trengrove, Ty Smith, Kaushik Gopal, Annyce Davis and Luis Artola.

We discussed various topics such as whether the productivity claims are real, code review in the AI era, deployment safety and tokennomics. I won't review my own answers. You can do that, and then reply and tell me where I was wrong.

Overall, I had a blast attending the conference. This was probably the best event I’ve attended in a while and the Android team really did a fantastic job brining the community together and being fantastic hosts. We even got some goodies that most of us weren’t expecting at all and I’m genuinely grateful to have the opportunity to attend ADS this year.

What if I told you that every attendee had their “look under your seat” Oprah moment? Crazy, isn’t it?

😆 Dev Delight

💸 🔥

Right?????? Right???????

🤔 Interesting tid-bits

🖼️ Compose Previews just escaped Android Studio

For those that have known me for a few years, you already know that I have a thing or two about Compose Previews. So this is a pretty big deal — the new android preview command builds your target and renders @Preview functions to PNGs headlessly on your machine. You don't need a device, an emulator, or a running Studio.

android preview list :app --all --format=json        # every @Preview, MultiPreviews included

android preview render :app \

--fqn=com.example.app.HomeScreenKt.HomeScreenPreview \

--component-tree --daemon -o build/previews/

Google pitches it as how agents check their own UI work, and says agents using it finished UI tasks 77% faster with up to 3x fewer tokens. That's believable. An agent that can see what it built stops guessing. But I think the bigger change is that previews now work anywhere the command runs. Here's why I'm excited:

  • --component-tree is the sleeper feature. It writes a JSON file next to each PNG with bounds, component names, source locations, semantics, parameters and modifiers. An agent (or a lint script) can confirm "the button has a content description and sits inside the card" without squinting at pixels. Said differently, you now have the ability to get the representation of a Composable in text format. The kind of things you can do with this information has no limits.

  • --daemon keeps the renderer warm for 10 minutes, so the build-render-look loop doesn't pay the cold-start cost every time.

  • Your previews just became infrastructure. Every rotten @Preview that only compiles because nobody opens that file is now a broken tool for your agent. I've been saying previews are a product surface, not an Android Studio convenience, for about six years, so forgive me one "told you so."

One caveat: this renders previews; it doesn't replace screenshot tests (but you can probably use it as part of that pipeline). There's no golden-image diffing here.

🤖 Studio stops picking your agent for you

Android Studio Rabbit 2 is stable with Bring Your Own Agent (BYOA): more than 40 agents over the open Agent Client Protocol, including Claude Agent, Codex, and Antigravity. This was a highly requested feature and offering this level of flexibility is a welcome change. I appreciate how truth seeking companies like Google and Jetbrains (by releasing the Kotling LSP) have been recently where they’ve made strategic calls in the best interest of the developer. Kudos!

🏗️ Lightbuild: Google's experiment in builds you can't "program"

Here's the problem it's going after. build.gradle.kts isn't really config. It's a Kotlin program that runs to produce config. That's why sync takes ages, why two modules that look identical can build differently, and why an agent editing your build is basically refactoring code it can't run in its head.

Lightbuild swaps the program for a “description”. You get one project.lightbuild.yaml at the root (modules, repositories, toolchain versions) and one lightbuild.yaml per module that says what the module is and what it needs:

# app-core/lightbuild.yaml
type: library
android:
  dependencies:
    maven:
      - com.squareup.moshi:moshi:1.15.2
  kspProcessors:            # KSP gets applied because you declared this
    maven:
      - com.squareup.moshi:moshi-kotlin-codegen:1.15.2

There's no plugins {} block, no afterEvaluate, and nowhere to put logic. You declare a module type and Lightbuild turns on the features your config needs. Dependencies are resolved ahead of time into a checked-in resolved.deps file per module, so opening and building the project skips dynamic resolution. In Studio Canary, there's no manual sync after you edit config. Builds run through a separate lightbuild CLI (lightbuild build app, lightbuild test app-core), and it works with Android CLI.

Google says this is for small apps, prototypes and demos, and that production apps should stay on Kotlin DSL. There’s a very long list of things it doesn't support: version catalogs, convention plugins, custom plugins, product flavors, KMP modules, or mixing Kotlin DSL and Lightbuild modules in one project. So treat this as an FYI rather than a call to action.

Why I still think it's worth ten minutes: it's Google saying out loud that the build file should be “data” an agent (or a person) can read and edit safely, not a program you have to execute to understand. Whether that's a feature or a deal-breaker for your team is the actual conversation to have. Only time will tell.

The bug is previous + 1. It treats a state tool like an event stream. A transform doesn't run once per write. It runs when the values it reads have changed by the time it gets a chance to look. (The Transform docs spell this out.)

  • Same photo again: setting openedPhoto to a value equal to what's already there isn't a change, so nothing recomposes and nothing gets counted.

  • Fast taps: if three photos get opened before the next recomposition, the transform only sees the last one. The two in between never existed as far as the transform knows.

The fix is to count the event where it happens, and let transform just read the result:

var photosOpened by mutableIntStateOf(0)

private set

fun onPhotoTapped(photo: Photo) {

    openedPhoto = photo
    // one tap, one increment
    photosOpened++ 

}

Google's docs list running totals, event logs and one-time commands as jobs for plain code or a Flow, because those need every value exactly once. Here's the rule I'd tape to the monitor: transform answers "what is true right now?" It can't answer "how many times did this happen?"

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

👂 Let me hear it!

What did you think of this email?

Login or Subscribe to participate in polls.

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

—

Sr Engineering Manager @ Databricks | Google Developer Expert for Android | ex-Airbnb, Snap, Spotify, Deloitte

Vinay Gaba is a Google Developer Expert for Android and serves as an Engineering Leader at Databricks, where he supports the AI Devtools team. His team's mission is to enhance developer productivity through cutting-edge tools leveraging LLMs and Generative AI. Vinay has deep expertise in AI Tooling, Android, UI Infrastructure, Developer Tooling, Design Systems and Figma Plugins. Prior to Databricks, he worked at Airbnb, Snapchat, Spotify, and Deloitte. Vinay holds a Master's degree in Computer Science from Columbia University.

Reply

or to participate.