Dispatch Issue #27


GM Friends. This is JetpackCompose.app's Dispatch. The #1 Doctor recommended source for your weekly Android knowledge.
This is Issue #27 and boy do we have a lot in store for you.
π₯ Tipsy Tip
The Android Agent Tooling Triumvirate
Google just dropped something that's going to quietly reshape how a lot of us work: a unified set of agentic tooling for Android development that works with any agent β Gemini, Claude Code, Codex, Antigravity, whatever you're into.
There are three pieces, and they're all worth knowing about:
Android CLI π οΈ
A real, official, regularly-updated CLI for Android development. Commands like:
android sdk installβ install only the components you needandroid createβ generate projects from official templatesandroid emulator / android runβ manage virtual devices and deployandroid updateβ keep your tooling fresh
Google claims it reduces LLM token usage by 70% and makes agent-driven tasks 3x faster versus letting an agent flail with raw shell commands. The underlying idea is solid: give the agent a constrained, predictable interface instead of letting it find / -name "*.gradle" 2>/dev/null its way through your filesystem. π¬
Android Knowledge Base π§
This is the piece I'm most excited about. Most LLMs have a training cutoff that's months (sometimes a year+) old. That's why your agent keeps suggesting Navigation 2 APIs when you're on Navigation 3.
The Android Knowledge Base gives agents two new tools:
search_android_docsβ searches authoritative Android docs (developer.android.com, Firebase, Kotlin docs, Google Developers)fetch_android_docsβ pulls full document content
It's already built into Gemini in Android Studio. And via Android CLI's android docs, any agent can use it. So when you're prompting Claude Code in your terminal, you can say "Upgrade navigation to Navigation 3. Refer to Android documentation for guidance" and it'll actually go fetch the latest official guidance instead of hallucinating from 2023 training data.
Android Skills π
Modular markdown instruction sets (SKILL.md files) that auto-trigger based on your prompt. These cover the gnarly workflows where LLMs typically suggest outdated patterns:
- Navigation 3 setup and migration
- Edge-to-edge implementation
- AGP 9 migration
- XML β Compose migration
- R8 config analysis
Browse and install via android skills. These can sit alongside your own CLAUDE.md or AGENTS.md β they're additive.
Survived an Android production crash? π«‘ bitdrift's Beyond the Noise is where mobile engineers tell the stories that never made the postmortem: crashes, ANRs, "how did this ship?" moments at scale, and AI's impact on how we build. With guests like Jesse Wilson, Kelsey Hightower, and many more. π§ Give it a listen.
πΎ Pop Quiz
Look at this code from the Kotlin stdlib:
public actual fun CharSequence.isBlank(): Boolean =
length == 0 || indices.all { this[it].isWhitespace() }Beautiful one-liner, right? Clean, idiomatic Kotlin written by the developers of the Kotlin language itself. What could be wrong about it?
Hint: It's not a correctness bug
You will find the answer in a section below β¬οΈ
π€Ώ Deep Dive
Compose 1.11 Is Stacked β Here's What Actually Matters
Every Compose release lately has been chunky, but the April '26 release (Compose 1.11) is genuinely one of the more substantive ones. Here are a few features that stand out for me.
π§ͺ Coroutine execution in tests
This is THE change that's going to trip up half the codebases out there. The v2 testing APIs are now the default, and v1 is deprecated.
Why does this matter? Previously, Compose tests used UnconfinedTestDispatcher, which executed coroutines immediately. That made tests feel synchronous and "just work" β but it also masked a LOT of race conditions that absolutely existed in production.
The new default is StandardTestDispatcher. Coroutines launched in your tests now queue up and only execute when you advance the virtual clock. Translation: half your "passing" tests are about to reveal they were relying on a happy accident. π
If you're running a large codebase, do yourself a favor: read the migration guide before you bump the BOM. Otherwise your CI is going to look like a Christmas tree, and not in the fun way π
ποΈ Styles API β modifiers, but make them declarative
This is super interesting to me because I spent a few years in a previous role on the Design Systems team at Airbnb and we missed the ability to do something like this.
With the new (experimental) Style API lets you customize components via a state-aware property block instead of stacking modifiers:
Button(
onClick = { /* ... */ },
style = {
background((listOf(lightPurple, lightBlue))
width(75.dp)
height(50.dp)
externalPadding(16.dp)
pressed {
background(listOf(Color.Magenta, Color.Red))
}
}
) { Text("Login") }That pressed { } block? Chef's kiss. π€
No more InteractionSource gymnastics for the 90% case. Google is reporting meaningful performance benefits here too, because styles can be applied without going through the full modifier chain reconciliation. Material components will eventually adopt Styles once the API stabilizes.
π¨ Grid and FlexBox β finally, real layout primitives
These are experimental but they're a BIG deal. We've been bending Row/Column/LazyVerticalGrid to do things they were never quite designed for. Now we get:
Grid β actual 2D layouts with tracks, gaps, cells, percentages, intrinsic sizes, and "Fr" units. If you've ever written CSS Grid, you know exactly why this is exciting.
@OptIn(ExperimentalGridApi::class)
@Composable
fun GridExample() {
Grid(
config = {
repeat(4) { column(0.25f) }
repeat(2) { row(0.5f) }
gap(16.dp)
}
) {
Card1(modifier = Modifier.gridItem(rowSpan = 2))
Card2(modifier = Modifier.gridItem(columnSpan = 3))
Card3(modifier = Modifier.gridItem(columnSpan = 2))
Card4()
}
}π¬ Preview wrappers
Custom previews with @PreviewWrapper finally let you stop wrapping every preview in your theme manually:
class ThemeWrapper : PreviewWrapper {
@Composable
override fun Wrap(content: @Composable (() -> Unit)) {
JetsnackTheme { content() }
}
}
@PreviewWrapperProvider(ThemeWrapper::class)
@Preview
@Composable
private fun ButtonPreview() {
Button(onClick = {}) { Text("Demo") }
}π This has been such a small quality-of-life win, but multiplied across hundreds of previews? BIG DEAL.
Now, wouldn't it be nice if every preview didn't need that extra annotation? I think so too β I should add native support for this in Showkase. If you are looking for open source projects to contribute to, hit me up!
π€ Interesting tid-bits
KMP finally gets a saner default structure π§±
JetBrains is changing the default Kotlin Multiplatform project structure, and honestly⦠this feels like one of those "boring but extremely important" changes.
The old world often had a composeApp module doing multiple jobs:
- shared KMP library
- Android app entry point
- desktop app bits
- web app bits
- Compose Multiplatform UI
- platform packaging config
- probably your hopes, dreams, and one cursed Gradle workaround from 2023
The new default is much cleaner:
shared/
androidApp/
desktopApp/
webApp/
iosApp/The big idea: shared code lives in a shared library module, runnable apps live in app modules.
This matters because Android Gradle Plugin 9.0 no longer supports applying the Android application plugin inside a multiplatform module. So for Android-targeting KMP projects, it's also future-proofing.
If you're using native UI on some platforms, the structure can split into:
sharedLogic/
sharedUI/Which I love because it makes the decision obvious:
- business rules, models, validation β
sharedLogic - Compose Multiplatform UI β
sharedUI
π¦ Community Spotlight
Friends at Software Mansion just open-sourced Pulsar, and it's honestly a most delightful library.
Pulsar is a haptics library with 150+ reusable haptic patterns for Android, iOS, React Native, Flutter, and Kotlin Multiplatform. There's a companion app that lets you connect your phone to their web playground and physically feel each preset before you wire it into your app.
Think about how absurdly good this DX is. Haptics are inherently impossible to design without a device β you can't preview them in Figma, you can't audition them via headphones (well, you can sort of, via audio approximation), and historically the only way to evaluate one was to write code, compile, deploy, feel, repeat. Pulsar collapses that loop to seconds.
The presets cover everything from subtle "tick" feedback for selection to elaborate accelerometer-driven sequences.
If you've been on the fence about adding haptics to your app, this is your no-excuses moment πͺ
πΎ Pop Quiz: Solution
Android OG Romain Guy was poking around the Jetpack Compose runtime and stumbled into something interesting.
That innocent-looking indices.all { } does NOT compile to a simple for loop. Because indices returns an IntRange and all { } is a generic extension on Iterable, the compiler:
- Allocates a new
IntRangeobject - Calls
iterator()on it, allocating anIntProgressionIterator - Iterates via the generic interface, with all the indirection that implies
For a list of 1,000 strings, the stdlib version allocates 2,001 objects and takes ~38,000ns. A simple manual for loop allocates zero and runs in ~15,000ns β 60% faster.
private inline fun CharSequence.fastIsBlank(): Boolean {
for (i in 0..<length) {
if (!this[i].isWhitespace()) return false
}
return true
}You can push it further by skipping the redundant Character.isSpaceChar() check that Char.isWhitespace() makes under the hood:
private inline fun CharSequence.fastIsBlank(): Boolean {
for (i in 0..<length) {
val c = this[i]
if (!Character.isWhitespace(c) &&
c != '\u00a0' && c != '\u2007' && c != '\u202f') {
return false
}
}
return true
}That gets you to ~13,500ns β 65% faster than stdlib, with zero allocations.
The lesson: This doesn't matter when you're validating a form field on a button click. It matters enormously when you're inside the Compose runtime processing strings on every recomposition, or parsing a large document. Innocent-looking one-liners can have outsized perf impact in hot paths. The key detail is hot paths β for most, this is a nice-to-know. Please don't find and replace all usages of isBlank in your codebase π
π¦ 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!
On that note, here's hoping that your bugs are minor and your compilations are error free.


