JetpackCompose.app's Dispatch Issue #23

šŸ’Œ In today’s Dispatch:🧾 Stop GPL surprises with Licensee ⚔ Gradle config speed rule 🤯 Compose effects order puzzle 🧠 AI vs git bisect

2025 State of Mobile Release Management

Runway's new report on mobile releases shows that automation alone isn't solving core issues: teams that invest significantly in automation still lose 6–10 hours per release to manual busywork and coordination overhead – about the same as teams with less automation in place

GM Friends. This is JetpackCompose.app’s Dispatch, the newsletter that arrives in your inbox with more performance-boosting energy than a perfectly configured Baseline Profile.

This is Issue # 23, so, silence that Slack notification for "just five more minutes," grab your favorite Friday morning/night alcoholic beverage, and let's dive in… hic šŸ˜µā€šŸ’«

šŸ„‚ Tipsy Tip

Here's a scenario that keeps legal teams up at night: you're building a closed-source product, and somewhere deep in your dependency graph, a GPL-licensed library sneaks in. By the time you discover it, you're weeks away from release. Panic ensues. Lawyers get involved. Your PM starts stress-eating donuts.

Enter Licensee, a brilliant Gradle plugin from the folks at Cash App that validates your dependency licenses before they become a problem.

Here's how it works: you define which licenses your project allows, and Licensee fails your build if anything violates those rules. It's like having a legal compliance officer built into your CI pipeline.

Setup is dead simple:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'app.cash.licensee:licensee-gradle-plugin:1.14.1'
  }
}

apply plugin: 'app.cash.licensee'

Then configure your allowed licenses:

licensee {
  allow('Apache-2.0')
  allow('MIT')
  allow('BSD-3-Clause')
}

Now when you run ./gradlew licensee, it'll scan your entire dependency graph and fail the build if anything doesn't match your allowlist. The plugin generates two incredibly useful reports:

  1. artifacts.json – A complete inventory of every dependency and its license

  2. validation.txt – A human-readable report showing what's allowed and what's not

But wait, there's more! You can also handle edge cases:

licensee {
  // Allow specific dependencies with wonky license data
  allowDependency('com.example', 'sketchy-lib', '1.0') {
    because 'Apache-2.0, but they typo'd the license URL'
  }
  
  // Ignore internal/commercial dependencies
  ignoreDependencies('com.mycompany.internal') {
    because 'internal closed-source libraries'
  }
  
  // Allow non-SPDX license URLs
  allowUrl('https://example.com/custom-license.html') {
    because 'Apache-2.0, but self-hosted'
  }
}

The because clauses are brilliant—they force you to document why you're making exceptions, which is exactly what your future self (or your legal team) will thank you for.

I’m confident most readers weren’t aware of the existence of this library. And if you help build apps of any reasonable size, this is a no-brainer integration that will allow you to shine at work 🪩 Pays to be the subscriber of this newsletter 🤣

Jokes aside, the plugin is actively maintained, works with Android, Kotlin Multiplatform, and standard JVM projects, and has saved countless developers from licensing nightmares. If you're not using it yet, you're playing with fire šŸ”„

Crashes are loud, leaks are quiet. Our latest hands-on guide shows how to spot memory leaks before your users feel the pain, using bitdrift’s lightweight SDK. Read the guide.

Consider the following code snippet—

@Composable
fun Example() {
    Log.d("Example", "First")
    SideEffect { Log.d("Example", "SideEffect") }
    DisposableEffect(Unit) {
        Log.d("Example", "DisposableEffect")
        onDispose {}
    }
    LaunchedEffect(Unit) { Log.d("Example", "LaunchedEffect") }
    Box(
        modifier = Modifier
            .onSizeChanged { Log.d("Example", "Layout") }
            .drawBehind { Log.d("Example", "Draw") }
    )
    Log.d("Example", "Last")
}

In what order do you expect to see the log statements printed out?

You will find the answer in a section below ā¬‡ļø

šŸ˜† Dev Delight

I found Kotlin’s very first public commit from 15 years ago. The file list? BinaryTree, Graph, Queue, UnionFind, ArrayList, Iterator, LinkedList, BinaryHeap, PriorityQueue. Looking at this list, you can't help but chuckle. It feels less like the birth of a pragmatic, modern programming language and more like someone was building the ultimate toolkit to absolutely demolish FAANG coding interviews šŸ˜…

There’s something delightful about a language that introduced itself with data structures before niceties. It’s like showing up to a potluck with a fully balanced red-black tree.

On a serious note: those primitives still echo in the way Kotlin encourages immutability and expressive algorithms today. Also, .jetl files! Early days are wild.

🤿 Deep Dive

What’s a ā€œnormalā€ Gradle configuration time, anyway?

Confession: most of us treat Gradle configuration like a weather pattern—unpredictable, mildly annoying, and definitely outside our control. It doesn’t have to be. You can (and should) know what ā€œhealthyā€ looks like and catch regressions before your team accepts slow configs as fate.

The excellent write‑up by Aurimas Liutikas titled ā€œUnreasonable Configuration — What is a Normal Amount of Gradle Configuration Time?ā€ makes a crisp, opinionated case: for a modern Apple Silicon laptop, a well‑maintained build’s configuration phase should be roughly 100ms per subproject on a config cache miss. If your build has 20 subprojects, ~2 seconds is a reasonable ballpark. If you’re seeing 9s, 20s, 26s…it’s time to pop the hood.

Why this rule of thumb works

  • The configuration phase is largely serial today (Isolated Projects is changing that, but most builds aren’t there yet). You’re gated by single‑core velocity, not parallelism.

  • Well‑behaved plugins follow ā€œtask configuration avoidanceā€ and defer expensive work to execution. If configuration is slow, someone’s doing too much up front.

Measure it correctly (no vibes, only receipts)

  • You want to time just initialization + configuration, not task execution. The simplest way:

    • Clear config cache: rm -fr .gradle/configuration-cache/

    • Dry run: ./gradlew build --dry-run

  • Better: use gradle-profiler to automate and smooth out variance. Aurimas provides a simple scenario file to isolate just the configuration phase with a clean cache:

Why does this matter?

Configuration runs every single time you sync, build, or run a task. If your configuration is slow, every interaction with your build system is slow. This compounds over hundreds of builds per day across your entire team.

Key takeaways:

  1. Measure your configuration time using the dry-run approach above

  2. Calculate your expected time (number of subprojects Ɨ 100ms)

  3. If you're more than 2-3x over, you've got work to do

  4. Profile to find the culprit (see Aurimas's post for profiling techniques)

  5. Common offenders: plugins that do I/O during configuration, eager task creation, and complex dependency resolution

Configuration phase should be lightweight and fast. If it's not, you're bleeding productivity every single day. Time to investigate šŸ”

šŸ”¦ Community Spotlight

I love git bisect. But on mobile, rebuilds are expensive, caches are fickle, and reproductions can be flaky. Matt McKenna argues that in many cases it’s faster to shovel the ā€œbig diffā€ between good and bad releases into an AI agent and ask: ā€œGiven the bug behavior, what looks suspicious?ā€ .

Where it shines

  • Repro is flaky or only on prod devices.

  • 1000+ commits between tags with slow builds.

  • Vague symptoms: ā€œsome screens stallā€ or ā€œmetrics dipped.ā€

Where bisect still wins

  • Minimal repro exists and is deterministic.

  • You have fast local builds and good caches.

  • You want the exact first bad commit as a fact in the postmortem.

This is one more concrete example where LLMs are changing what our day to day looks like and it’s a super important tool in our arsenal to use where the situation demands it.

Heres’s the order in which the print statements are printed—

First
Last
DisposableEffect
SideEffect
Layout
Draw
LaunchedEffect

Why This Order?

1. Composition Phase (First, Last)

First
Last

These log statements execute during the composition phase - when Compose is reading your @Composable function to build/update the UI tree. This phase runs synchronously and completely before any effects execute.

2. Effects Execute After Composition

After composition completes, Compose processes effects in a specific order:

DisposableEffect Before SideEffect

DisposableEffect  // "Remembered effect"
SideEffect        // "Side effect"

This is the key insight! Compose has two categories of effects:

  • Remembered effects (DisposableEffect, LaunchedEffect, produceState, etc.) - These are effects that remember state and can be disposed

  • Side effects (SideEffect) - These run after every successful recomposition

This makes sense architecturally: SideEffect is designed to publish Compose state to non-Compose code, so it should run after all Compose-managed effects have set themselves up.

Layout and Draw

Layout  // onSizeChanged
Draw    // drawBehind

These run during the layout and draw phases of the standard Android rendering pipeline, which happens after composition and effects.

LaunchedEffect Comes Last

LaunchedEffect

LaunchedEffect is special - it launches a coroutine, and by default uses Dispatchers.Main.immediate (this is the case currently and the behavior was different pre Compose 1.0). However, the body of the coroutine doesn't execute immediately. Here's why:

  • LaunchedEffect sets up during the effects phase (along with DisposableEffect)

  • But the coroutine body itself is dispatched to the main thread's message queue

  • This means it runs after the current frame's composition, layout, and draw are complete

Think of it as: "The effect is registered now, but the work happens in the next message loop iteration."

Thanks to Nicklas Ansman for asking this brainteaser in ASG and enlightening us with new insights.

šŸ¦„ 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 ā˜•ļø All our kind supporters get featured here.

šŸ‘‰šŸ»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’d you think of this email? Tap your choice below and lemme hear it šŸ‘‡

šŸš€šŸš€šŸš€šŸš€šŸš€ AMAZING-LOVED IT!
šŸš€šŸš€šŸš€ PRETTY GOOD!

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

—

Tech Lead Manager @ Airbnb | Google Developer Expert for Android | ex-Snap, Spotify, Deloitte

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

Reply

or to participate.