EmergeTools logo
🙏 Supported by Emerge Tools
The platform for all your mobile performance needs

Dispatch Issue # 1: Inagural Edition 🚀

Vinay Gaba Profile Image
Vinay Gaba on May 09, 2024
Hero Image

Good Morning Friend! Welcome to the very first edition of JetpackCompose.app’s Dispatch 💌 Think of us as your Android barista – we know your order, and yes, we add the extra shot of clarity.

🎩 Insider Insight

Today’s Insider Insight is about how the structure of your Jetpack Compose code can impact your app’s performance. Consider this example, where we aim to implement a series of bars made up of multiple colored dots.

Individual Composables Visualization A bar visualization that we want to implement in Jetpack Compose

Initially, developers might opt to create a Composable function for each individual dot, leading to a straightforward but performance-costly implementation:

@Composable
fun Visualization(bars: ImmutableList<Bar>) {
    Row {
        bars.forEach {
            Bar(it)
        }
    }
}

@Composable
fun Bar(bar: Bar) {
    ....
    Column {
        for(i in 0..numBarRows) {
            Row {
                for (i in 0..4) {
                    Dot(...)
                }
            }
        }
    }
}

@Composable
fun Dot(color: Color) {
    Box(...) {...}
}

This approach is intuitive and readable, yet it results in approximately 350 Composable functions that the framework needs to manage, significantly increasing the workload across all phases of the composable lifecycle: composition, layout, and drawing.

An alternative strategy is to draw all dots in a bar directly on the Canvas using a single Bar Composable.

Aggregated Composables Visualization In this approach, each bar represented by the yellow border is responsible for drawing all the dots within it directly on the Canvas

@Composable
fun Visualization(bars: ImmutableList<Bar>) {
    Row {
        bars.forEach {
            Bar(it)
        }
    }
}

@Composable
fun Bar(bar: Bar) {
    Canvas() {
        for (i in 0 until bar.numRows) {
            for (j in 0 until bar.numColumns) {
                drawCircle(....)
            }
        }
    }
}

This method drastically reduces the number of Composables, significantly decreasing resource use during the critical phases:

  1. Composition: Fewer Composables streamline setup and management.
  2. Layout: A simplified layout phase cuts down computation time.
  3. Drawing: Fewer Composables mean faster rendering, as the system can batch draw calls more efficiently.

This optimization by consolidating multiple elements into fewer Composables can significantly enhance performance and user experience. As you design your Compose layouts, consider the benefits of grouping UI elements strategically.

Trade-offs: This isn’t a silver bullet. Always weigh the benefits against the potential drawbacks. In scenarios where the performance gain is substantial, like our example, adopting a more complex but efficient method is advantageous. However, if the performance improvement is marginal, prioritizing code readability and ease of implementation might be more beneficial.

Hat-tip to my friend and colleague Felipe Roriz who first brought this up during a conversation and got to the bottom of it.

😆 Dev Delight

Modifier Imports We’ve all been frustrated by this. Thankfully, you can avoid this by following the steps from this article that I wrote a couple years ago.

Thinking about Java WHY!!!
Source

Want to get similar insights, tips & entertainment directly in your inbox? Subscribe to Dispatch, the newsletter that quenches your Android thirst with a splash of Compose and a twist of humor.
JetpackCompose.app Mascot
No spam whatsoever and one click unsubscribe if you don't like it.

🤔 Interesting tid-bits

  1. After a few false starts and nearly two years since the Compose 1.0 release, shared element transitions have finally landed in Compose with the 1.7.0-alpha07+ release 🥳 Last week, Google published a guide showcasing these APIs, and they are absolutely BEAUTIFUL 😍. The animation APIs in Compose have set a new standard, significantly enhancing user experience over the traditional view system. This new functionality adds yet another layer of easy-to-use, eye-catching transitions, marking a leap forward for the Compose story.
  2. Last week, the Jetpack Compose compiler codebase packed its bags and moved into the Kotlin repo. This intriguing move has positive implications for us developers. No more wrangling with compatibility maps to find which Kotlin version pairs with the Compose compiler—hallelujah! 🥳 From Kotlin 2.0 onward, Compose and Kotlin will release together, sharing the same version number. So, the Compose Compiler is jumping to 2.0 to catch up. A couple of other nifty changes come along for the ride: Maven coordinates are now org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable, and no more fiddling with kotlinCompilerExtensionVersion—thank you, universe! Here's a handy Pull Request showing just how seamless this switch will be in practice.
  3. The Google Drive team published a case study about how they rewrote their home screen in Jetpack Compose and completed it in half the estimated time. The Compose version required 57% less feature code and 76% less test code while adding net new features and meaningfully increasing test coverage. Now that's what I call doing more with less! Someone promote the entire team already 😂 The interesting bit here is how a planned redesign is the perfect excuse to hop on the Compose train. It's a tried-and-true strategy among hundreds of companies over the past few years, ensuring they don't make enemies out of their feature developers with dreaded forced migrations. Why force it when you can glide right into something better?

💻 Code Corner

Have you ever wondered why your Composable is recomposing due to a state read? Sometimes it’s hard to tell even if you are using the Recomposition count tracker that’s embedded into the Layout Inspector in Android Studio. Well you don’t need to guess anymore because Andrei Shikov has you covered. His snippet of code makes it easy for you to know the exact state that’s causing recompositions from the logs that it emits. It’s super easy for you to use as well -

// Just wrap the composable that you need more
// information about with the following Composable

DebugStateObservation("MyCustomComposable") {
    MyCustomComposable(...)
}

That’s all the setup that it needs! Check it out -

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.MutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot
import java.util.WeakHashMap
@PublishedApi
internal class DebugStateObservation(private val id: String) {
private val map = WeakHashMap<Any, MutableList<Exception>>()
val readObserver: (Any) -> Unit = {
synchronized(this) {
val e = Exception()
val list = map.getOrPut(it) { mutableListOf() }
list += e
}
}
fun print(changes: Set<Any>) {
synchronized(this) {
val affected = map.keys.intersect(changes)
if (affected.isNotEmpty()) {
affected.forEach {
printStateChange(id, it, map[it])
}
}
}
}
fun clear() {
synchronized(this) {
map.clear()
}
}
}
private fun printStateChange(id: String, state: Any, exceptions: List<Exception>?) {
val traces = exceptions?.joinToString(separator = "\n") {
// remove trace start, sample:
// at androidx.compose.foundation.demos.DebugStateObservation$readObserver$1.invoke(Test.kt:33)
// at androidx.compose.foundation.demos.DebugStateObservation$readObserver$1.invoke(Test.kt:31)
// at androidx.compose.runtime.snapshots.SnapshotKt$mergedReadObserver$1.invoke(Snapshot.kt:1771)
// at androidx.compose.runtime.snapshots.SnapshotKt$mergedReadObserver$1.invoke(Snapshot.kt:1770)
// at androidx.compose.runtime.snapshots.SnapshotKt.readable(Snapshot.kt:2003)
// at androidx.compose.runtime.SnapshotMutableIntStateImpl.getIntValue(SnapshotIntState.kt:138)
val stackTrace = it.stackTrace
buildString {
for (i in 6.. minOf(10, stackTrace.size)) {
append("\tat ${it.stackTrace[i]}")
append("\n")
}
append("...")
}
} ?: ""
println("$id might recompose because $state changed, last read at:\n$traces")
}
/**
* Records state observations inside @Composable [block] and prints to [System.out] whenever
* state mutation is applied.
*
* NOTE: This doesn't record recompositions precisely and only uses snapshot system to record state
* mutations that /might/ invalidate recomposition. Consecutive invocations might result in
* different results depending on functions that were run / skipped during each execution. To be
* used directly inside a function scope that recomposes, as Compose might skip inner scopes and
* reads/mutations are not going to be recorded.
*/
@Composable
inline fun <T> DebugStateChanges(id: String, block: @Composable () -> T): T {
val observation = remember { DebugStateObservation(id) }
val currentSnapshot = Snapshot.current
val snapshot = if (currentSnapshot is MutableSnapshot) {
currentSnapshot.takeNestedMutableSnapshot(observation.readObserver)
} else {
currentSnapshot.takeNestedSnapshot(observation.readObserver)
}
DisposableEffect(observation) {
val disposeHandle = Snapshot.registerApplyObserver { changes, _ ->
observation.print(changes)
}
onDispose {
observation.clear()
disposeHandle.dispose()
}
}
observation.clear()
return snapshot.runAndDispose { block() }
}
// Compose doesn't work with try/finally, but we don't really use it for catching things.
@PublishedApi
internal inline fun <T> Snapshot.runAndDispose(block: () -> T): T =
try {
enter(block)
} finally {
dispose()
}

🔦 Community Spotlight

In today's community spotlight, we're shining a light on a couple of projects that have boldly stepped into the void left open in the Compose ecosystem: Markdown rendering! These projects are more relevant than ever in today's chat-first era, where LLMs churn out Markdown-rich text like it's going out of style. Formatting and parsing their output is crucial for many apps these days, making these projects absolute gems.

  1. MarkdownTwain
  2. compose-markdown

The community has been clamoring for a native solution, as seen in this issue, where 115 folks have enthusiastically thrown in their +1s. The good news is that the change landed last month, meaning Markdown rendering will soon be natively supported in Compose 🥳. But hey, if Google builds in the functionality you created a library for, you get to wear that badge of honor as a maintainer—consider it a compliment!


Vinay Gaba Profile Image
Vinay Gaba is a Google Developer Expert in Android and currently serves as a Tech Lead Manager at Airbnb, where he spearheads the UI Tooling Foundation team. His team’s mission is to enhance developer productivity through cutting-edge tools leveraging LLMs and Generative AI. With a career spanning ~14 years, Vinay has deep expertise in UI Infrastructure, Developer Tooling, Design Systems and Figma Plugins. Prior to Airbnb, he contributed to the technological advancements at Snapchat, Spotify, and Deloitte. Vinay holds a Master's degree in Computer Science from Columbia University and has been at the forefront of Android development since 2011
If you like this article, you might also like:
Think of us as your Android barista – we know your order, and yes, we add the extra shot of clarity
JetpackCompose.app Mascot
No spam whatsoever and one click unsubscribe if you don't like it.