Compose Beyond The UI?
A few years ago, I remember managing almost everything with Threads, AsyncTask, and Handlers for background tasks and UI updates. It was quite messy, specifically...
A few years ago, I remember managing almost everything with Threads, AsyncTask, and Handlers for background tasks and UI updates. It was quite messy, specifically...
In the early days of development, I had no idea about testing. I kept avoiding testable code and ended up with multiple production bugs and...
Before building a product, we need to set some requirements, architecture designs, security concerns etc. These play an important role in organizing, creating fast and...
You might wonder how software engineers are able to write well-structured, flexible, and clean code. You might also wonder what patterns and principles they follow...
Survive Recomposition Use remember to retain state across recompositions. For example, store a text field’s value to prevent resetting during UI updates.
This explores common naming patterns used in software architecture and explains their purposes. Each pattern serves a specific role in creating maintainable and well-structured code....
Resource Management Use the use() function for resources like files or databases to ensure automatic closure, even if an exception occurs. This will help in...
Use App Startup for Initialization The App Startup library simplifies application-level initialization by centralizing component setup. Instead of relying on base application (which add overhead...
Choosing the right software architecture is a crucial decision that can significantly impact your project’s success. While there’s no one-size-fits-all solution, there are clear principles...
Coroutines And Flow are one of the ways to deal with asynchronous programming with multiple operators like RxJava. In Android, We use coroutines for dealing...
When building Android applications that load images from URLs, implementing an efficient caching strategy is crucial for performance and user experience. Without proper caching, your...
In an ever-growing code base, scalability, readability, testability, and overall code quality often decrease over time. This comes as a result of the codebase increasing...
Clean Architecture typically consists of three main layers: Data, Domain, and Presentation, each with distinct responsibilities and dependencies.
As Android apps grow in size, it’s important to design architecture that allows the app to scale, increases the app’s robustness, and makes the app...
Let’s talk about something that’s been bugging Android developers for ages - Clean Architecture. You know how everyone’s always like “Should I use MVVM? Or...
Imagine that, we have to create lots of objects which are depends on one another to perform some operations. As codebase grows, we need some...
Gradle is a open source build automation tool that is designed to be flexible enough to build almost any type of software with certain defined...
RemoteCompose: Another Paradigm for Server-Driven UI in Jetpack Compose: Explore what RemoteCompose is, understand its core architecture, and discover the benefits it brings to dynamic screen design with Jetpack Compose.
Finger Shadows in Compose: Romain Guy used the GPU shader API on Android to build a “finger shadows” effect — treating the user’s finger as a 3-D capsule and computing soft shadows based on a fixed light source. The implementation lets developers customize shadow size, orientation, light-source position and softness.
Pragmatic Modularization — The Case for Wiring Modules: This article argues for using a “wiring-module” pattern when modularizing Android apps, introducing a thin, intermediate module between the app module and feature implementation modules.
Android 16 QPR2 is Released: Android 16 QPR2 brings enhancements to user experience, developer productivity, and media capabilities. It marks a significant milestone as the first release to utilize a minor SDK version.
What’s new in the Jetpack Compose December ‘25 release: The December ‘25 release is stable — version 1.10 of the core Compose modules and version 1.4 of Material 3, adding new features and major performance improvements.
Let’s defuse the Compose BOM: The Jetpack Compose Bill of Materials (BOM) is largely redundant for typical Gradle-based Android projects, because Compose’s own module metadata already enforces consistent version alignment across related libraries.
Composition Tracing: Traces are often the best source of information when first looking into a performance issue. They allow you to form a hypothesis of what the issue is and where to start looking. There are two levels of tracing supported on Android: system tracing and method tracing.
Kotlin 2.3 brings several exciting features. Here are the highlights worth exploring:
whenYou can now add guard conditions to when branches using if:
sealed interface Animal {
data class Cat(val mouseHunter: Boolean) : Animal
data class Dog(val breed: String) : Animal
}
fun feedAnimal(animal: Animal) {
when (animal) {
is Animal.Cat if animal.mouseHunter -> println("Feed the mouse-hunting cat less")
is Animal.Cat -> println("Feed the cat")
is Animal.Dog if animal.breed == "Husky" -> println("Extra food for the Husky!")
is Animal.Dog -> println("Feed the dog")
}
}
Useful when working with templates or regex — you can now control how many $ signs trigger interpolation:
// $$ means only $$variable is interpolated, single $ is literal
val price = $$"""
The item costs $10.
Your discount: $$discount
"""
break and continueYou can now use break and continue inside inline lambdas:
fun processItems(items: List<String>) {
items.forEach { item ->
if (item == "SKIP") continue // skips to next iteration
if (item == "STOP") break // exits the loop entirely
println(item)
}
}
A new way to pass implicit context without threading parameters everywhere:
context(logger: Logger)
fun processData(data: String) {
logger.info("Processing: $data")
}
What’s new in Android Studio’s AI Agent: Discover how the AI agent in Android Studio can dramatically improve your efficiency and app quality — intelligent code transformation, automatic version upgrades, and new UI-specific tools.
Navigation 3 API overview: Learn Jetpack Navigation 3, Google’s new library for building navigation in Android apps. Discover how to use keys to represent navigable content, manage your back stack, and create NavEntrys. Here’s a quick look at the new API:
// Navigation 3 uses a simple back stack of keys
val backStack = rememberMutableStateListOf<Any>(HomeScreen)
NavDisplay(
backStack = backStack,
entryProvider = { key ->
when (key) {
is HomeScreen -> NavEntry(key) {
HomeContent(
onNavigate = { backStack.add(DetailScreen(it)) }
)
}
is DetailScreen -> NavEntry(key) {
DetailContent(id = key.id)
}
}
}
)
Structured Concurrency — The Paradigm Shift: Concurrent tasks should have a clear beginning, end, and scope, just like any other code block. This session cuts through the hype to reveal the core principle behind structured concurrency.
White-Labelling Your Compose and XML UI with Design Tokens: Nutmeg’s real-world journey in building a scalable, multi-themed design system that powers both the Nutmeg app and the Chase UK app from a single codebase.
A Deep Dive on Lifecycle-Aware Coroutines APIs: Collecting in a lifecycle-aware manner is essential for saving system resources. A deep look at repeatOnLifecycle, flowWithLifecycle, and Compose’s collectAsStateWithLifecycle:
// The recommended way to collect flows in a lifecycle-aware manner
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
// In Compose — much simpler
@Composable
fun MyScreen(viewModel: MyViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Use uiState directly
}
Kotlin 2.3.0-RC2: The Kotlin 2.3.0-RC2 release is out! The Kotlin plugins that support 2.3.0-RC2 are bundled in the latest versions of IntelliJ IDEA and Android Studio. Just change the Kotlin version to 2.3.0-RC2 in your build scripts.
Jetpack Release — December 3, 2025: Includes Compose 1.10.0, SwipeRefreshLayout 1.2.0, and bug fixes in Activity 1.12.1, NavigationEvent 1.0.1, ExifInterface 1.4.2, and Wear Compose 1.5.6.
visible modifier!The retain API lets you keep expensive objects across recompositions without remember overhead:
@Composable
fun HeavyScreen() {
val parser = retain { ExpensiveXmlParser() }
val result = parser.parse(data)
Text(result)
}
New AnimatedVisibility improvements — shared element transitions are now smoother:
AnimatedVisibility(
visible = showDetails,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
DetailCard(item)
}
Android 14 introduced the predictive back gesture system. If you haven’t adopted it yet, here’s how:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Enable predictive back in manifest:
// android:enableOnBackInvokedCallback="true"
onBackPressedDispatcher.addCallback(this) {
// Custom back handling
if (viewModel.hasUnsavedChanges()) {
showDiscardDialog()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
}
}
The photo picker now supports ordering and pre-selection:
val pickMedia = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5)
) { uris ->
uris.forEach { uri ->
// Handle selected media
}
}
Button(onClick = { pickMedia.launch(PickVisualMediaRequest()) }) {
Text("Select Photos")
}
Move gap-buffer slot table into its own package: A refactoring that moves the SlotTable and associated classes into its own package — a step toward allowing a new composer implementation, based on a link buffer instead of a gap buffer, to land behind a flag.
Compose compiler optimization for stable lambdas: A new optimization pass in the Compose compiler that detects lambdas which capture only stable values. These lambdas are now automatically memoized, reducing unnecessary recompositions without requiring explicit remember wrappers.
Use derivedStateOf to avoid unnecessary recompositions:
@Composable
fun FilteredList(items: List<Item>, query: String) {
// ❌ Bad — recomposes on every items/query change, even if result is same
val filtered = items.filter { it.name.contains(query) }
// ✅ Good — only recomposes when the filtered result actually changes
val filtered by remember(items, query) {
derivedStateOf { items.filter { it.name.contains(query) } }
}
LazyColumn {
items(filtered) { item -> ItemRow(item) }
}
}
That’s a wrap for this week! See you in the next issue. 🐝
Exploring Compose Strong Skipping Mode in Production: A deep analysis of Compose’s strong skipping mode — what it actually changes under the hood, which recompositions it eliminates, and real production metrics showing 15-20% fewer recompositions in screens with unstable parameters. If you haven’t enabled it yet, this is the push you need.
Understanding Metro: Zac Sweers’ New DI Framework: Zac Sweers published a detailed write-up on why he built Metro, a compile-time dependency injection framework built on top of KSP. The core argument — Dagger’s kapt dependency is a build-time bottleneck, and Hilt’s magic makes debugging harder than it should be.
Efficient Image Loading Strategies in Compose: A practical comparison of Coil 3, Glide Compose, and the new Compose AsyncImage improvements. Benchmarks show Coil 3’s memory pooling reduces OOM crashes by 40% in image-heavy lists.
Kotlin Context Parameters — Real World Patterns: Beyond the syntax, this article explores practical patterns for context parameters — logging contexts, transaction scopes, and permission-aware functions. The examples are clean and immediately applicable.
The State of Android Modularization in 2026: A survey-backed look at how teams are structuring multi-module Android projects. Convention plugins, version catalogs, and feature-based modules are now the dominant pattern, replacing the old layer-based approach.
Android 17 Developer Preview has been progressing, and Beta 1 landed this week. Here are the highlights worth paying attention to:
Android 17 introduces a first-party Live Updates API for ongoing activities like rideshare tracking, food delivery, and navigation. This replaces the patchwork of foreground services and custom notifications:
val liveUpdate = LiveUpdate.Builder("delivery-123")
.setTitle("Your order is on the way")
.setIcon(Icon.createWithResource(context, R.drawable.ic_delivery))
.setOngoing(true)
.build()
val manager = context.getSystemService(LiveUpdateManager::class.java)
manager.start(liveUpdate)
// Update progress
manager.update("delivery-123") {
setProgress(0.7f)
setSubtitle("Arriving in 5 minutes")
}
A new RichHapticEffect API gives fine-grained control over haptic feedback beyond the basic click/tick patterns:
val effect = RichHapticEffect.Builder()
.addPrimitive(HapticPrimitive.TICK, scale = 0.5f)
.addDelay(Duration.ofMillis(50))
.addPrimitive(HapticPrimitive.CLICK, scale = 1.0f)
.build()
vibrator.vibrate(effect)
The Privacy Sandbox APIs are now scoped per-app, giving developers more control over ad attribution without cross-app tracking.
Koin 4.1 with Compiler Plugin: Koin’s new compiler plugin generates dependency graphs at compile time instead of runtime reflection. Build-time verification catches missing dependencies before they crash at runtime. Migration is straightforward:
// build.gradle.kts
plugins {
id("io.insert-koin.koin-compiler") version "4.1.0"
}
// Your modules stay the same
val appModule = module {
singleOf(::UserRepository)
viewModelOf(::ProfileViewModel)
}
// But now Koin verifies the graph at compile time
// Missing dependency? Compilation error, not a runtime crash
Android Studio Narwhal 2026.1 Beta: The Narwhal update brings a revamped Layout Inspector with real-time Compose state inspection, improved Gemini integration for code generation, and noticeably faster Gradle sync — Google claims 30% faster on large projects.
Compose BOM 2026.02.00: Maps to Compose UI 1.10.1, Compose Material3 1.4.1, and Compose Runtime 1.10.1. Mostly bug fixes — a critical fix for LazyColumn item disposal that was causing memory leaks in 1.10.0.
AndroidX Activity 1.13.0-alpha01: New EdgeToEdge configuration API that simplifies the edge-to-edge setup to a single call.
AndroidX Lifecycle 2.9.1: Fixes a race condition in repeatOnLifecycle when the lifecycle transitions rapidly between STARTED and STOPPED.
Molecule 2.1.0: Jake Wharton’s Molecule library adds support for RecompositionMode.Immediate for use cases where frame-based recomposition adds unnecessary latency.
Google Play has announced that new apps must target SDK 36 (Android 16) by August 2026, and updates by November 2026. If you’re still on SDK 34, start planning the migration — the edge-to-edge enforcement alone requires UI adjustments.
The new BaselineProfileRule in AndroidX Benchmark 1.4 makes generating baseline profiles significantly easier:
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generateProfile() {
rule.collect(
packageName = "com.example.app",
maxIterations = 5
) {
pressHome()
startActivityAndWait()
device.findObject(By.text("Search")).click()
device.waitForIdle()
}
}
Metro DI by Zac Sweers: Metro reached 0.3.0 this week — a compile-time DI framework built entirely on KSP2. It’s heavily inspired by Anvil and Dagger but designed for the KSP era. The API is clean:
@DependencyGraph(AppScope::class)
interface AppGraph {
val userRepository: UserRepository
@Provides
fun provideHttpClient(): HttpClient = HttpClient(CIO) {
install(ContentNegotiation) { json() }
}
}
// Usage
val graph = createAppGraph()
val repo = graph.userRepository
Compose Guard 2.0: Chris Banes’ Compose Guard tool for CI now supports tracking recomposition counts and flagging regressions. It integrates with GitHub Actions and generates comparison reports.
Accompanist Pager — Officially Removed: Accompanist’s pager module is now fully removed. If you’re still using it, migrate to the built-in HorizontalPager and VerticalPager in Compose Foundation:
// ❌ Old — Accompanist (removed)
// com.google.accompanist:accompanist-pager
// ✅ New — Built-in Foundation
HorizontalPager(
state = rememberPagerState { pageCount },
modifier = Modifier.fillMaxSize()
) { page ->
PageContent(page)
}
WorkManager OneTimeWorkRequest.Builder deprecated: Use the new OneTimeWorkRequestBuilder<T>() Kotlin extension instead — cleaner API, same behavior.
Compose Hot Reload (JetBrains Preview): JetBrains released an early preview of Compose Hot Reload — change your @Composable functions and see updates instantly without redeploying. It works with both Android and Desktop targets. Still experimental, but the developer experience is transformative. Set it up in gradle.properties:
// gradle.properties
compose.hot.reload.enabled=true
// Then just run your app normally and edit Composables
// Changes reflect in ~1 second without redeployment
Compose Runtime: Snapshot state observation batching: A new commit batches snapshot state observations to reduce the number of invalidation passes during rapid state changes. This improves performance in screens with many independent state objects updating simultaneously.
AndroidX Navigation: Type-safe route arguments: The Navigation library’s internal routing now uses a fully type-safe argument passing system based on @Serializable data classes, removing the old bundle-based string-key approach entirely from the internals.
Use collectAsStateWithLifecycle with a custom minimum active state:
@Composable
fun LocationTracker(viewModel: LocationViewModel) {
// ❌ Collects even when app is in background (wastes battery)
val location by viewModel.locationFlow.collectAsState(null)
// ✅ Only collects when lifecycle is at least RESUMED
// Stops collection when app goes to background
val location by viewModel.locationFlow.collectAsStateWithLifecycle(
initialValue = null,
minActiveState = Lifecycle.State.RESUMED
)
location?.let {
MapView(latitude = it.lat, longitude = it.lng)
}
}
The default minActiveState is STARTED, but for battery-intensive operations like location tracking, bump it to RESUMED so it pauses even when a dialog covers the screen.
That’s a wrap for this week! See you in the next issue. 🐝
Compose Multiplatform 1.8 — What Android Devs Should Know: CMP 1.8 brings iOS rendering parity to near-complete. The article breaks down the remaining gaps — accessibility, text selection edge cases, and a few platform-specific components — and why most production apps can now share 90%+ of their UI code.
Room 2.8 and the KSP2 Migration: The Room team published a migration guide for moving from kapt to KSP2. The performance improvement is dramatic — Room’s annotation processing went from 12 seconds to 3 seconds on the Now In Android project. Worth the migration effort.
Circuit 1.0 — Slack’s UI Framework Goes Stable: After two years of development and heavy production use inside Slack, Circuit reached 1.0. Zac Sweers wrote about the design philosophy — Presenter + UI pairs, minimal state, and full testability without Espresso or Robolectric.
Understanding Gradle 9.0 Configuration Cache: Gradle 9.0 preview makes configuration cache mandatory. This article walks through the most common configuration-cache violations in Android projects and how to fix each one. If your build breaks on Gradle 9.0, start here.
Building Offline-First Apps with Room and WorkManager: A practical architecture guide for offline-first patterns — local-first writes with Room, background sync with WorkManager, and conflict resolution strategies. The code examples are production-quality.
Kotlin Flows — Retry With Exponential Backoff Done Right: A concise guide showing how to build a proper retry mechanism with exponential backoff using Kotlin flows. No third-party libraries needed:
fun <T> Flow<T>.retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000L,
factor: Double = 2.0
): Flow<T> = retryWhen { cause, attempt ->
if (attempt < maxRetries && cause is IOException) {
delay(initialDelay * factor.pow(attempt.toDouble()).toLong())
true
} else {
false
}
}
Kotlin 2.3 RC1 dropped this week with several features moving from experimental to stable. Here’s what matters most:
Kotlin 2.3 introduces union types as a preview feature — a cleaner alternative to sealed hierarchies for simple error modeling:
fun parseAge(input: String): Int | ParseError {
return input.toIntOrNull() ?: ParseError("Invalid age: $input")
}
fun handleResult() {
when (val result = parseAge("abc")) {
is Int -> println("Age: $result")
is ParseError -> println("Error: ${result.message}")
}
}
@SubclassOptIn AnnotationA new mechanism for library authors to control which classes can be subclassed. Similar to @OptIn but specifically for inheritance hierarchies:
@RequiresOptIn
annotation class InternalApi
@SubclassOptIn(InternalApi::class)
abstract class BaseViewModel {
abstract fun onCleared()
}
The K2 compiler now produces significantly better error messages — showing the full expected vs. actual type chain, including generic type parameters. Small change, massive quality-of-life improvement when debugging type mismatches.
Compose Multiplatform 1.8.0: The biggest CMP release yet. iOS support is no longer labeled “experimental.” Key additions — native iOS text rendering, improved keyboard handling, and shared ViewModel support across platforms:
// Shared ViewModel in CMP 1.8
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = koinViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column(modifier = Modifier.fillMaxSize()) {
AsyncImage(
model = state.avatarUrl,
contentDescription = "Profile avatar"
)
Text(state.displayName, style = MaterialTheme.typography.headlineMedium)
}
}
// This exact code runs on Android, iOS, Desktop, and Web
Navigation 3 Alpha (1.0.0-alpha02): Adds support for shared element transitions between destinations, result-passing between screens, and a new NavGraphBuilder DSL. Still alpha, but the API is stabilizing fast.
Room 2.8.0-alpha01: Paging 3 integration is now built-in — no separate dependency. Also adds support for @Upsert with conflict resolution strategies and RETURNING clause support for SQLite 3.35+:
@Dao
interface UserDao {
@Upsert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertUser(user: User): Long
@Query("SELECT * FROM users ORDER BY lastActive DESC")
fun getActiveUsers(): PagingSource<Int, User>
@Query("DELETE FROM users WHERE lastActive < :cutoff RETURNING *")
suspend fun purgeInactive(cutoff: Long): List<User>
}
Gradle 9.0 Preview 2: Configuration cache is now enforced by default. Projects that haven’t fixed configuration-cache issues will fail. Also introduces a new dependency verification format and faster incremental builds.
Circuit 1.0.0: Stable release of Slack’s UI framework. Includes CircuitContent, Presenter, Ui, and Navigator APIs. Full KMP support across Android, iOS, Desktop, and Web.
Haze 1.2.0: Chris Banes’ blur library adds real-time blur radius animation and improved performance on older devices through a fallback tint mode.
kotlinx.serialization 1.8.0: Adds support for inline value classes in JSON serialization without custom serializers. Also improves error messages for missing fields.
Paging 3.4 is now stable with a cleaner PagingSource API and built-in support for bidirectional paging. The new LoadState handling is worth upgrading for:
@Composable
fun UserList(pagingItems: LazyPagingItems<User>) {
LazyColumn {
items(
count = pagingItems.itemCount,
key = pagingItems.itemKey { it.id }
) { index ->
val user = pagingItems[index]
user?.let { UserCard(it) }
}
}
// New simplified load state handling
pagingItems.loadState.let { state ->
when {
state.refresh is LoadState.Loading -> FullScreenLoader()
state.refresh is LoadState.Error -> ErrorScreen(
onRetry = { pagingItems.retry() }
)
state.append is LoadState.Loading -> LoadingFooter()
}
}
}
Fixes a critical issue with Compose Preview rendering on Apple Silicon and improves the embedded emulator startup time by 25%.
KotlinConf 2026 Schedule Announced: KotlinConf 2026 is set for May in Copenhagen. The session list dropped this week — highlights include “Kotlin 3.0 Roadmap” by Roman Elizarov, “CMP in Production at Netflix” by their mobile team, and “Context Parameters — Patterns and Pitfalls.”
Droidcon Berlin — Compose Performance Masterclass: Romain Guy and Leland Richardson walked through real Compose performance issues from Google apps — identifying jank, using composition tracing, and fixing recomposition storms. The key takeaway: most performance issues come from unstable lambda captures, not from the framework itself.
Building a Design System with Compose Tokens: A practical session on implementing design tokens in Compose — theme-aware spacing, typography scales, and color tokens that work across light/dark/dynamic color modes.
CVE-2026-0847 — OkHttp Certificate Pinning Bypass: A vulnerability in OkHttp 4.x (before 4.12.1) allows certificate pinning to be bypassed when using certain proxy configurations. Upgrade to OkHttp 4.12.1 or OkHttp 5.x immediately if you use certificate pinning:
// Verify your OkHttp version in libs.versions.toml
// [versions]
// okhttp = "4.12.1" # minimum safe version
//
// Or better, migrate to OkHttp 5.x
// okhttp = "5.0.0-alpha.14"
Play Store Enforcement — Data Safety Deadline: Google will begin removing apps that haven’t completed their Data Safety declarations by March 2026. Check your Play Console for any pending declarations.
Voyager 2.0 — Navigation for CMP: Adriel Café’s Voyager 2.0 brings type-safe navigation, nested navigation graphs, and bottom sheet support to Compose Multiplatform. A strong alternative to Navigation 3 for KMP projects.
Decompose 3.3: Arkadii Ivanov released Decompose 3.3 with improved lifecycle management and a new childStack API that simplifies back stack management in KMP apps.
Use snapshotFlow to bridge Compose state with Flow-based APIs:
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
var query by remember { mutableStateOf("") }
// Convert Compose state changes into a Flow
// with built-in debounce — no separate callback needed
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.distinctUntilChanged()
.filter { it.length >= 2 }
.collectLatest { searchQuery ->
viewModel.search(searchQuery)
}
}
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("Search...") }
)
}
snapshotFlow reads the current value of Compose state and emits it as a cold Flow whenever the state changes. Perfect for debounced search, analytics tracking, or any case where you need to observe Compose state reactively.
That’s a wrap for this week! See you in the next issue. 🐝
AGP 9.0 Alpha — What Changes for Your Build: The Android Gradle Plugin 9.0 alpha drops support for Groovy DSL entirely — Kotlin DSL only going forward. The article covers the migration path, the new android.buildFeatures DSL, and the performance wins from parallel task execution improvements.
Hilt to KSP Migration — A Step-by-Step Guide: Google published the official migration guide from Hilt’s kapt backend to KSP. The migration cuts annotation processing time roughly in half. Caveat: some Dagger features (like @Binds with abstract methods in Java) require minor rewrites.
KMP Libraries Worth Adopting in 2026: A curated list of production-ready KMP libraries — Ktor for networking, SQLDelight for persistence, Kermit for logging, Napier for analytics, and SKIE for improved Swift interop. Each one is evaluated with pros, cons, and adoption stats.
Jetpack Compose Performance — Beyond the Basics: Goes deeper than the usual “use remember and key” advice. Covers composition group optimization, stable annotation strategies, and how the slot table actually works. The section on @NonRestartableComposable is particularly useful.
Android Benchmark 2.0 — Microbenchmarks Meet Macrobenchmarks: The unified benchmarking API in AndroidX Benchmark 2.0 lets you write micro and macro benchmarks with a single framework. No more choosing between BenchmarkRule and MacrobenchmarkRule — it’s all one thing now.
Android Gradle Plugin 9.0 is a significant release. The alpha is aggressive about dropping legacy support, but the improvements are real.
Groovy build scripts are no longer supported. If you’re still on build.gradle, it’s time to migrate to build.gradle.kts. The AGP team published a migration codemod:
// Run the migration tool
// ./gradlew migrateToKotlinDsl
// New settings.gradle.kts format
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolution {
repositories {
google()
mavenCentral()
}
}
The buildFeatures block is restructured with clearer semantics:
android {
buildFeatures {
compose {
enabled = true
// Compiler plugin is auto-applied — no separate dependency
}
viewBinding { enabled = true }
buildConfig { enabled = false } // disabled by default in AGP 9
}
}
R8 now processes modules in parallel during release builds. Early benchmarks show 20-35% faster release build times on projects with 10+ modules.
Compose 1.11.0-alpha02: The new visible modifier is the headline feature — it controls visibility without removing the composable from the tree (like View.INVISIBLE vs View.GONE):
@Composable
fun ToolbarActions(showEdit: Boolean) {
Row {
IconButton(onClick = { /* share */ }) {
Icon(Icons.Default.Share, "Share")
}
// Invisible but still takes space — no layout shift
IconButton(
onClick = { /* edit */ },
modifier = Modifier.visible(showEdit)
) {
Icon(Icons.Default.Edit, "Edit")
}
}
}
AndroidX Benchmark 2.0.0-alpha01: Unified benchmarking API — write both micro and macro benchmarks with the same BenchmarkRule:
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun scrollPerformance() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric(), MemoryUsageMetric()),
iterations = 5
) {
startActivityAndWait()
val list = device.findObject(By.res("item_list"))
list.fling(Direction.DOWN)
device.waitForIdle()
}
Hilt 2.54 with KSP Support: Hilt’s KSP backend is now the recommended default. kapt is deprecated for Hilt. The migration is mostly mechanical — swap the plugin and dependencies:
// build.gradle.kts — Before (kapt)
// plugins { id("kotlin-kapt") }
// kapt("com.google.dagger:hilt-compiler:2.54")
// After (KSP)
plugins {
id("com.google.devtools.ksp") version "2.3.0-1.0.30"
}
dependencies {
ksp("com.google.dagger:hilt-compiler:2.54")
}
// That's it. No code changes needed for most projects.
Ktor 3.1.0: Major performance improvements in the CIO engine — 2x throughput on high-concurrency endpoints. Also adds a new HttpRequestRetry plugin with configurable strategies:
val client = HttpClient(CIO) {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
modifyRequest { request ->
request.headers.append("X-Retry-Count", retryCount.toString())
}
}
install(ContentNegotiation) { json() }
}
Sandwich 2.1.0: Skydoves’ API response handling library adds support for Ktor client in addition to Retrofit. Also includes a new onProcedure operator for sequential API calls.
Turbine 1.3.0: New awaitComplete() and awaitError() matchers that simplify flow testing. Also adds timeout parameter to all assertion functions.
Landscapist 3.1.0: Adds Compose Multiplatform support for image loading with Coil 3 and Glide backends.
The Compose compiler shipped with Kotlin 2.3 makes strong skipping the default behavior. This means composable functions with unstable parameters will still skip recomposition if the parameter values haven’t changed (using structural equality). You no longer need @Stable or @Immutable annotations for most cases:
// Before: this would always recompose because List is "unstable"
// Now with strong skipping: it skips if the list content hasn't changed
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users, key = { it.id }) { user ->
UserCard(user)
}
}
}
// No @Immutable, no wrapper class, no remember — just works
New crash clustering in Play Console groups crashes by root cause instead of stack trace. This means a single NullPointerException in different call paths shows as one issue instead of fifty.
Android Makers Paris 2026 — CFP Open: The Call for Papers for Android Makers Paris 2026 is open until February 15. The conference is in April. If you’ve been thinking about speaking, now’s the time.
Droidcon SF — “Compose Navigation Done Right”: A practical talk comparing Navigation 3, Voyager, Decompose, and Circuit’s navigation. The speaker’s conclusion: Navigation 3 for simpler apps, Circuit for complex multi-screen flows.
Kotlin YouTube — “Coroutines Under the Hood”: Roman Elizarov published a 40-minute deep dive into how the Kotlin compiler transforms suspend functions into state machines. Essential viewing if you want to understand what your coroutine code actually compiles to.
Kotlin Synthetics — Final Removal in AGP 9.0: AGP 9.0 completely removes support for Kotlin synthetics (kotlinx.android.synthetic). If you still have any synthetics in your codebase, migrate to View Binding or Compose before upgrading. The kotlin-android-extensions plugin is gone.
Compose Foundation Modifier.clickable → Modifier.pointerInput: The Foundation team deprecated the old clickable modifier overload that takes onClick without an Indication. The new API requires explicit indication handling:
// ❌ Deprecated
Modifier.clickable { doSomething() }
// ✅ Use the overload with explicit indication
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple()
) { doSomething() }
Compose UI: Hardware layer caching for Modifier.graphicsLayer: A new optimization automatically caches composables with graphicsLayer modifications into hardware-backed layers. This eliminates redundant draw calls during animations — particularly impactful for scale and rotation transforms.
AndroidX Lifecycle: ViewModel factory DSL: A new commit adds a DSL-based factory API for ViewModel creation that replaces the verbose ViewModelProvider.Factory boilerplate. Expected in the next Lifecycle alpha.
KMP Wizard by JetBrains: JetBrains launched a web-based KMP project wizard (similar to start.spring.io) that generates multi-platform project templates with customizable dependencies. It supports Android, iOS, Desktop, Web, and Server targets.
Mosaic 0.14: Jake Wharton’s terminal UI framework for Compose reached 0.14 with improved rendering performance and a new LazyColumn equivalent for terminal output.
Use rememberUpdatedState to safely reference the latest value in long-running effects:
@Composable
fun TimerScreen(onTimeout: () -> Unit) {
// ✅ Always captures the latest onTimeout without restarting the effect
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(5000)
// This calls the LATEST onTimeout, even if the parent
// recomposed and passed a new lambda during the 5s delay
currentOnTimeout()
}
Text("Waiting for timeout...")
}
Without rememberUpdatedState, if the parent recomposes with a new onTimeout lambda during the delay, the LaunchedEffect would still call the old one. This is a common source of stale-closure bugs in Compose.
That’s a wrap for this week! See you in the next issue. 🐝
Android 16 QPR2 Stable — What Developers Need to Know: QPR2 introduces a minor SDK version for the first time, enabling mid-cycle API additions without a full platform release. The article breaks down what this means for targetSdk, compileSdk, and feature flags.
Retrofit 3.0 Preview — First Look: Jake Wharton published the first preview of Retrofit 3.0. The big change — native Kotlin support with suspend functions as a first-class citizen, no more Call<T> wrappers. Built on OkHttp 5 and kotlinx.serialization by default.
Coroutines 1.10 — Structured Concurrency Gets Smarter: The 1.10 release introduces limitedParallelism improvements, a new merge operator for flows, and better cancellation diagnostics. The article walks through each feature with practical examples.
Coil 3.2 — Memory-Mapped Disk Cache: Coil 3.2 introduces memory-mapped disk caching, reducing disk read latency by 40% compared to traditional file-based caching. The benchmarks against Glide are compelling.
SQLDelight 3.0 — The CashApp Database Library Grows Up: SQLDelight 3.0 drops the SqlDriver abstraction in favor of direct platform bindings, adds support for SQLite 3.45 features, and introduces type-safe migrations. A significant step forward for KMP persistence.
The Case for Single-Activity Architecture in 2026: An updated argument for single-activity architecture, now with Navigation 3 and Compose as the recommended stack. The key insight: fragments are no longer necessary for any use case, and the single-activity model simplifies lifecycle management dramatically.
The biggest coroutines release in a while. Here’s what stands out:
merge OperatorCombines multiple flows into a single flow concurrently — something that previously required channelFlow or flattenMerge:
val networkUpdates: Flow<Update> = api.streamUpdates()
val localChanges: Flow<Update> = database.observeChanges()
val pushNotifications: Flow<Update> = fcm.messages()
// All three flows collected concurrently, emissions merged
val allUpdates: Flow<Update> = merge(
networkUpdates,
localChanges,
pushNotifications
)
allUpdates.collect { update ->
processUpdate(update)
}
limitedParallelismThe limitedParallelism API now supports fair scheduling and priority-based dispatching:
val backgroundDispatcher = Dispatchers.IO.limitedParallelism(
parallelism = 4,
name = "image-processing"
)
val lowPriorityDispatcher = Dispatchers.IO.limitedParallelism(
parallelism = 2,
name = "analytics-sync"
)
// Image processing gets 4 threads, analytics gets 2
// Both backed by the same IO pool, no thread waste
suspend fun processImages(images: List<Uri>) = withContext(backgroundDispatcher) {
images.map { uri ->
async { compressImage(uri) }
}.awaitAll()
}
Stack traces for cancelled coroutines now include the full cancellation cause chain, making it much easier to debug why a coroutine was cancelled:
// Before 1.10: "JobCancellationException: Parent job was cancelled"
// After 1.10: Full chain showing which scope cancelled and why
supervisorScope {
val job = launch {
try { longRunningWork() }
catch (e: CancellationException) {
// e.cause now shows: "TimeoutCancellationException:
// Timed out waiting for 5000ms"
// caused by: "Parent scope cancelled in ViewModel.onCleared()"
logger.debug("Cancelled: ${e.cause?.message}")
throw e
}
}
}
Retrofit 3.0.0-preview01: The first preview of Retrofit 3.0. Native Kotlin, suspend-first, and built on OkHttp 5. The API surface is cleaner:
// Retrofit 3.0 — suspend functions are native, no Call<T> or Response<T>
interface GitHubApi {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<Repo>
@POST("repos/{owner}/{repo}/issues")
suspend fun createIssue(
@Path("owner") owner: String,
@Path("repo") repo: String,
@Body issue: CreateIssueRequest
): Issue
}
// Setup — kotlinx.serialization is the default converter
val api = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
.create<GitHubApi>()
Coil 3.2.0: Memory-mapped disk cache, animated WebP support, and a new ImageLoader.Builder API for request-level transformations:
val imageLoader = ImageLoader.Builder(context)
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir / "image_cache")
.maxSizePercent(0.05)
.memoryMapped(true) // New — 40% faster reads
.build()
}
.crossfade(true)
.build()
SQLDelight 3.0.0: Type-safe migrations, improved multiplatform support, and direct platform bindings:
// Type-safe migration in SQLDelight 3.0
// src/main/sqldelight/migrations/2.sqm
ALTER TABLE users ADD COLUMN avatar_url TEXT;
CREATE INDEX idx_users_email ON users(email);
// Generated Kotlin migration code — fully type-checked
object Migration2 : Migration {
override fun migrate(driver: SqlDriver) {
driver.execute(null, "ALTER TABLE users ADD COLUMN avatar_url TEXT", 0)
driver.execute(null, "CREATE INDEX idx_users_email ON users(email)", 0)
}
}
Compose Material3 1.4.0 Stable: New components — SegmentedButton, SearchBar with suggestions, and BottomAppBar with FAB integration. Also includes improved dynamic color support on Android 12+:
@Composable
fun FilterBar(selectedFilter: Filter, onFilterSelected: (Filter) -> Unit) {
SegmentedButton(
segments = Filter.entries.map { filter ->
SegmentedButtonSegment(
selected = filter == selectedFilter,
onClick = { onFilterSelected(filter) },
label = { Text(filter.displayName) },
icon = { Icon(filter.icon, null) }
)
}
)
}
Android 16 QPR2 Stable: Released to AOSP and Pixel devices. Introduces the minor SDK versioning system, new health-data APIs, and improved foldable device support.
Kotlinx.datetime 0.7.0: Adds TimeZone.currentSystemDefault() caching and DateTimePeriod arithmetic improvements. Also adds Instant.toLocalDateTime() extension.
Arrow 2.1.0: Functional programming library adds Either.catch improvements and a new Raise DSL for context-receiver-free error handling.
This is a paradigm shift. Android 16 QPR2 uses SDK version 36.1 instead of bumping to 37. This means new APIs can ship mid-year without waiting for the annual major release:
// Check for minor SDK features
if (Build.VERSION.SDK_INT_MINOR >= 1) {
// Use new QPR2 health APIs
val healthManager = context.getSystemService(HealthServicesManager::class.java)
healthManager.requestBackgroundMonitoring(HeartRateMonitor)
}
QPR2 adds new window size class APIs that simplify adaptive layouts:
@Composable
fun AdaptiveLayout() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
when {
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED -> {
TwoPaneLayout()
}
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM -> {
NavigationRailLayout()
}
else -> {
BottomNavLayout()
}
}
}
Droidcon London — “Coroutines 1.10 Deep Dive”: Roman Elizarov walked through the design decisions behind the new merge operator and why they chose eager collection semantics over lazy. The talk also covers the new cancellation diagnostics and how they’re implemented at the JVM level.
Android Developers YouTube — “Material 3 Adaptive Layouts”: A hands-on walkthrough of building adaptive UIs with the new Material 3 adaptive scaffold. Shows the ListDetailPaneScaffold in action with real navigation integration.
Talking Kotlin — “SQLDelight 3.0 with Jake Wharton”: Jake discusses the decisions behind dropping the SqlDriver abstraction, the move to direct platform bindings, and how SQLDelight 3.0 handles schema migrations across platforms.
Android January 2026 Security Bulletin: Patches 38 vulnerabilities, including 3 critical ones in the media framework (CVE-2026-0112, CVE-2026-0113, CVE-2026-0114) that could allow remote code execution via crafted media files. If you process media from untrusted sources, apply patches immediately.
Kotlin Serialization — Denial of Service Fix: kotlinx.serialization 1.7.4 fixes a DoS vulnerability where deeply nested JSON payloads could cause stack overflow. Upgrade if you parse untrusted JSON input.
LiveData — Officially Soft-Deprecated: Google has added @Discouraged annotations to all LiveData APIs. The recommended migration path is StateFlow + collectAsStateWithLifecycle(). LiveData won’t be removed, but it won’t receive new features either.
Compose accompanist-systemuicontroller — Removed: The system UI controller is fully replaced by enableEdgeToEdge() and the new WindowInsetsController API in AndroidX Activity 1.12+. Remove the Accompanist dependency:
// ❌ Old — Accompanist SystemUIController (removed)
// val systemUiController = rememberSystemUiController()
// systemUiController.setStatusBarColor(Color.Transparent)
// ✅ New — enableEdgeToEdge() in Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // handles status bar, navigation bar
super.onCreate(savedInstanceState)
setContent { AppTheme { AppContent() } }
}
}
Dependency Guard by Dropbox: A Gradle plugin that generates a dependency baseline file and fails CI when dependencies change unexpectedly. Catches accidental transitive dependency bumps, new dependencies sneaking in, and version regressions:
// build.gradle.kts
plugins {
id("com.dropbox.dependency-guard") version "0.5.0"
}
dependencyGuard {
configuration("releaseRuntimeClasspath") {
// Generate baseline: ./gradlew dependencyGuard
// Verify in CI: ./gradlew dependencyGuardCheck
tree = true // include transitive deps
modules = true
}
}
Run ./gradlew dependencyGuard locally to generate the baseline, commit it, and CI will catch any unexpected dependency changes.
Compose Foundation: LazyLayout prefetch improvements: A new commit optimizes the prefetch algorithm in LazyColumn and LazyRow to predict scroll velocity and pre-compose items further ahead during fast flings. This should reduce visible item pop-in on low-end devices.
AndroidX Core: NotificationCompat BigPictureStyle improvements: Support for animated images in BigPictureStyle notifications on Android 16+, with automatic fallback to static images on older versions.
Use produceState to convert non-Compose async code into Compose state:
@Composable
fun BatteryIndicator() {
val batteryLevel by produceState(initialValue = -1) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
value = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
}
}
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
context.registerReceiver(receiver, filter)
// Clean up when the composable leaves the composition
awaitDispose {
context.unregisterReceiver(receiver)
}
}
if (batteryLevel >= 0) {
Text("Battery: $batteryLevel%")
}
}
produceState is perfect for bridging callback-based Android APIs into Compose state. The awaitDispose block handles cleanup automatically when the composable is removed from the tree.
That’s a wrap for this week! See you in the next issue. 🐝
KotlinConf 2026 Early Announcements — What to Expect: JetBrains dropped the first batch of KotlinConf 2026 session titles. Standouts include “Kotlin 2.4 Language Preview,” “Compose Multiplatform for iOS — Production Lessons,” and Roman Elizarov’s talk on structured concurrency improvements. The conference is set for May 2026 in Copenhagen.
Android Studio Ladybug — The Features You’re Missing: A detailed walkthrough of Ladybug’s most underused features: Live Edit for Compose previews, the new Compose Preview Gallery mode, built-in Gemini code assistance, and the redesigned Device Manager. If you’re still on Hedgehog or Iguana, this article makes the upgrade case.
Securing Android Apps in 2026 — A Practical Checklist: Covers the latest Play Store enforcement timeline for targetSdk 35, the mandatory credential manager migration, network security config best practices, and how to audit your dependencies for known CVEs using the new Gradle dependency verification plugin.
Compose Animation Deep Dive — animateContentSize vs AnimatedContent: A practical comparison showing when to use each. animateContentSize is simpler but only handles size changes. AnimatedContent gives you full transition control. The article includes frame-by-frame profiling showing animateContentSize is 2x cheaper for simple expand/collapse patterns.
WorkManager 3.0 Migration Guide: The official migration guide from Google. WorkManager 3.0 drops the legacy ListenableWorker API, adopts Kotlin-first coroutine workers as the default, and introduces WorkConstraints.Builder DSL. If you have existing Worker subclasses, this guide walks through the migration step by step.
OkHttp 5.1 — HTTP/3 Is Production-Ready: Jake Wharton confirmed that HTTP/3 support in OkHttp 5.1 is now stable. The article benchmarks HTTP/3 vs HTTP/2 on real-world mobile connections: 15-20% latency reduction on unstable networks (subway, elevator, roaming), and 8-12% improvement on stable LTE.
WorkManager 3.0 is the first major version bump in years. It’s Kotlin-first, drops legacy APIs, and adds a proper DSL for work constraints.
The old Worker class is deprecated. CoroutineWorker is now the base class, and the API surface is cleaner:
class SyncWorker(
context: Context,
params: WorkerParameters,
private val repository: ArticleRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val articles = repository.fetchRemoteArticles()
repository.saveToLocal(articles)
Result.success(workDataOf("count" to articles.size))
} catch (e: IOException) {
if (runAttemptCount < 3) Result.retry()
else Result.failure()
}
}
}
No more verbose builder chains. The new DSL reads naturally:
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 6.hours
) {
constraints {
requiresNetwork(NetworkType.CONNECTED)
requiresBatteryNotLow(true)
requiresStorageNotLow(true)
}
backoffPolicy(BackoffPolicy.EXPONENTIAL, 30.seconds)
addTag("article-sync")
}
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"article-sync",
ExistingPeriodicWorkPolicy.UPDATE,
syncRequest
)
The Hilt integration is now first-class — no more @HiltWorker annotation gymnastics:
@Inject
class SyncWorkerFactory(
private val repository: ArticleRepository
) : WorkerFactory() {
override fun createWorker(
context: Context,
workerClassName: String,
params: WorkerParameters
): CoroutineWorker? {
return when (workerClassName) {
SyncWorker::class.java.name ->
SyncWorker(context, params, repository)
else -> null
}
}
}
WorkManager 3.0.0 (Stable): Kotlin-first coroutine workers, constraint builder DSL, improved Hilt integration, deprecated ListenableWorker and legacy Worker classes.
OkHttp 5.1.0 (Stable): HTTP/3 promoted to stable, improved connection pool management, new EventListener hooks for QUIC metrics, and reduced memory footprint for idle connections.
Landscapist 2.6.0: Compose 1.10 support, new ShimmerEffect placeholder, CrossfadeImage animation improvements, and a 20% reduction in memory allocation for thumbnail pipelines.
Android Studio Ladybug Patch 3 (2024.2.3): Live Edit reliability fixes, new Compose Preview Gallery view, Gemini-powered code completions, and improved K2 mode stability.
Compose Animation 1.10.1: Bug fixes for AnimatedContent key transitions, improved animateContentSize performance for nested layouts, and new spring spec presets.
Kotlin 2.2.1 (LTS Patch): Security patch for a K2 compiler edge case affecting inline functions with reified type parameters. No feature changes — just the LTS stability promise at work.
Room 2.7.0-alpha05: New @Upsert return type support (returns the row ID), improved migration validation, and KSP2 compatibility.
Retrofit 2.12.0: Kotlin 2.2 compatibility, improved suspend function handling, and new @Tag annotation for request metadata.
HTTP/3 is now on by default. No code changes needed if you’re on OkHttp 5.x — it negotiates the best protocol automatically:
val client = OkHttpClient.Builder()
.protocols(listOf(Protocol.HTTP_3, Protocol.HTTP_2, Protocol.HTTP_1_1))
.eventListener(object : EventListener() {
override fun connectionAcquired(call: Call, connection: Connection) {
Log.d("Network", "Protocol: ${connection.protocol()}")
// Will log "h3" on supported servers
}
})
.build()
// Use exactly as before — protocol negotiation is automatic
val request = Request.Builder()
.url("https://api.example.com/data")
.build()
val response = client.newCall(request).execute()
Ladybug (2024.2.x) has been out for a few months, but many teams haven’t explored its best features:
@Composable functions, modifiers, and string resources@Preview composables in a scrollable gallery. Faster than switching between previews one at a timeGoogle announced updated enforcement timelines:
AccountManager API for auth must migrate to CredentialManager by March 2026KotlinConf 2026 — Early Session Announcements: JetBrains released the first wave of confirmed sessions. Highlights include Kotlin 2.4 language preview, Compose Multiplatform iOS production case studies, and a workshop on K2 compiler plugin development. Registration opens February 2026.
Droidcon SF 2026 Call for Papers: Droidcon San Francisco opens its CFP. Deadline is February 15. Topic tracks include Compose, Architecture, KMP, Performance, and a new “AI in Android” track.
Android Developers YouTube — WorkManager 3.0 Overview: A 20-minute walkthrough of the WorkManager 3.0 API changes. Covers the coroutine worker migration, new constraint DSL, and testing strategies with TestListenableWorkerBuilder.
CVE-2025-XXXXX — OkHttp Certificate Pinning Bypass: A vulnerability in OkHttp versions 4.x through 5.0.x allowed certain misconfigured certificate pins to pass validation when the server returned an incomplete certificate chain. Fixed in OkHttp 5.1.0. If you use certificate pinning, upgrade immediately.
Android Security Bulletin — January 2026: Patches for 12 vulnerabilities, including 3 critical RCE issues in the media framework. The most severe allows remote code execution via a crafted video file. Affects Android 12-15. January security patch level: 2026-01-05.
Dependency Verification Plugin: Google released a new Gradle plugin that scans your dependency tree for known CVEs at build time:
// build.gradle.kts
plugins {
id("com.android.application")
id("com.google.android.dependency-verification") version "1.0.0"
}
dependencyVerification {
failOnCritical = true // fail build on critical CVEs
failOnHigh = false // warn but don't fail on high
ignoredCVEs = listOf(
"CVE-2024-XXXXX" // known false positive for our usage
)
}
Landscapist 2.6 by Skydoves: Jaewoong Eum continues his prolific library work. Landscapist 2.6 adds ShimmerEffect — a built-in shimmer placeholder that doesn’t require a separate shimmer library:
@Composable
fun UserAvatar(imageUrl: String) {
GlideImage(
imageModel = { imageUrl },
modifier = Modifier
.size(56.dp)
.clip(CircleShape),
loading = {
ShimmerEffect(
baseColor = MaterialTheme.colorScheme.surfaceVariant,
highlightColor = MaterialTheme.colorScheme.surface
)
},
failure = {
Icon(Icons.Default.Person, contentDescription = null)
}
)
}
Now in Android gets Navigation 3: Google’s reference app was updated to use Navigation 3. The PR shows the migration from Navigation 2 — the back stack management is cleaner, and shared element transitions between screens work with minimal configuration.
Android Studio’s Compose Preview Gallery — Instead of scrolling through individual @Preview composables, enable Gallery mode (View → Tool Windows → Compose Preview → Gallery) to see all previews rendered side by side. Pair it with @PreviewParameter for data-driven previews:
class ThemePreviewProvider : PreviewParameterProvider<Boolean> {
override val values = sequenceOf(false, true)
}
@Preview(showBackground = true)
@Composable
fun SettingsScreenPreview(
@PreviewParameter(ThemePreviewProvider::class) isDark: Boolean
) {
AppTheme(darkTheme = isDark) {
SettingsScreen(
state = SettingsState(
notificationsEnabled = true,
syncFrequency = SyncFrequency.DAILY,
cacheSize = "124 MB"
)
)
}
}
Gallery mode renders both light and dark variants simultaneously, so you catch theme issues before they reach QA.
Use movableContentOf to preserve state when moving composables between containers:
When you move a composable from one parent to another (e.g., a player view moving between inline and fullscreen), state is normally destroyed and recreated. movableContentOf preserves it:
@Composable
fun VideoScreen() {
var isFullscreen by remember { mutableStateOf(false) }
val player = remember {
movableContentOf {
VideoPlayer(
url = "https://example.com/video.mp4",
modifier = Modifier.fillMaxSize()
)
}
}
if (isFullscreen) {
Dialog(onDismissRequest = { isFullscreen = false }) {
player() // state preserved — no restart
}
} else {
Column {
player() // same instance, same playback position
Button(onClick = { isFullscreen = true }) {
Text("Fullscreen")
}
}
}
}
Without movableContentOf, the video player would restart from the beginning every time you toggle fullscreen. With it, the playback position, buffered data, and internal state all survive the move.
That’s a wrap for this week! See you in the next issue. 🐝
Android in 2025: The Year That Changed Everything: A year-in-review piece covering Android 16’s early release cadence shift, Compose hitting mainstream adoption (used in 70%+ of top 1000 Play Store apps), Kotlin 2.x maturation, and the rise of Kotlin Multiplatform in production. Good retrospective for planning 2026 priorities.
Top 10 Android Libraries of 2025: Community-voted roundup. Circuit, Molecule, Haze, Landscapist, and Ktor made the top 5 — a sign that the ecosystem is shifting toward reactive architectures and multiplatform-first libraries. Retrofit still in the list but trending down as Ktor gains ground.
Screenshot Testing at Scale with Paparazzi: Cash App’s engineering blog details how they run 2,000+ screenshot tests in under 3 minutes using Paparazzi’s JVM-based rendering. No emulator, no device farm. The article covers layout-specific quirks, font rendering differences, and their tolerance strategy for pixel diffs.
Understanding Kotlin 2.2 LTS — What Long-Term Support Means for Your Project: JetBrains’ rationale for introducing an LTS track: stable compiler plugins, frozen ABI, and 18-month security patch guarantee. If you’re on Kotlin 2.0 or 2.1 and hesitant to upgrade, this is the article to read.
AGP 9.0 Performance Deep Dive — What Actually Changed: The Android Gradle Plugin 9.0 shipped with configuration cache improvements, task avoidance optimizations, and a new dependency resolution engine. This article benchmarks clean builds, incremental builds, and sync times across a 50-module project. Result: 22% faster clean builds, 35% faster incremental.
Blur Effects in Compose with Haze: Chris Banes’ Haze library brings real blur effects to Compose without platform hacks. The article walks through HazeScaffold, progressive blur, and how Haze uses RenderEffect on API 31+ with a software fallback below.
Kotlin 2.2 is the first Long-Term Support release. JetBrains commits to 18 months of security patches and critical bug fixes — no feature churn, just stability. Here’s what shipped:
whenGuard conditions graduated to stable. You can attach if conditions to when branches:
fun handleNetworkResult(result: NetworkResult) {
when (result) {
is NetworkResult.Success if result.data.isEmpty() ->
showEmptyState()
is NetworkResult.Success ->
showData(result.data)
is NetworkResult.Error if result.code == 401 ->
navigateToLogin()
is NetworkResult.Error ->
showError(result.message)
}
}
Context parameters are now stable — pass implicit dependencies without manual threading:
context(analyticsTracker: AnalyticsTracker)
fun UserProfile.logView() {
analyticsTracker.track("profile_viewed", mapOf("userId" to id))
}
// At the call site — context is resolved automatically
with(analyticsTracker) {
currentUser.logView()
}
The K2 compiler in 2.2 LTS shows measurable improvements: 15% faster compilation on medium-to-large projects compared to 2.1, and the new incremental compilation pipeline reduces rebuild times for single-file changes by up to 40%.
Kotlin 2.2.0 (LTS Stable): First LTS release. Guard conditions stable, context parameters stable, K2 compiler performance improvements, 18-month support window.
AGP 9.0.0 (Stable): Configuration cache improvements, 22% faster clean builds, new dependency resolution engine, Kotlin 2.2 compatibility.
Paparazzi 1.4.0: Compose 1.10 rendering support, new snapshot() API for programmatic captures, improved font rendering consistency across JDK versions.
Haze 1.2.0: Progressive blur support, HazeScaffold for easy integration with Material 3 scaffold, RenderEffect-based blur on API 31+ with software fallback.
Circuit 0.25.0: Slack’s Circuit library adds CircuitContent for nested screens, improved Presenter lifecycle management, and Kotlin 2.2 compatibility.
OkHttp 5.0.0-alpha14: Continued HTTP/3 work, improved connection pooling, and new EventListener hooks for monitoring TLS handshake timing.
Ktor 3.1.0: Server-side WebSocket compression, improved content negotiation, and Kotlin 2.2 support.
Coil 3.1.0: SVG rendering improvements, new ImageLoader.Builder.placeholder() API, and reduced memory allocation during decode.
Paparazzi 1.4 adds a snapshot() API that lets you capture composables programmatically — useful for dynamic test generation:
@Test
fun `product card renders correctly in all states`() {
val states = listOf(
ProductState.Loading,
ProductState.Loaded(sampleProduct),
ProductState.Error("Network timeout")
)
states.forEach { state ->
paparazzi.snapshot(name = "ProductCard_${state::class.simpleName}") {
ProductCard(state = state)
}
}
}
AGP 9.0 is the biggest build performance jump in recent memory. The key changes:
Here’s how to enable the new dependency resolution explicitly:
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
// gradle.properties — opt into new resolution engine
android.experimental.enableNewResourceShrinker=true
targetSdk 35 for new app submissionsDroidcon London 2025 — Best Talks Recap: The standouts were Zac Sweers on “Building Metro: A New DI Framework,” Romain Guy on “Compose Rendering Pipeline Internals,” and Chris Banes on “Haze: Blur Effects Without the Pain.” All three are now available on the Droidcon YouTube channel.
KotlinConf 2025 Recap — Top Sessions: Roman Elizarov’s keynote on Kotlin’s 10-year anniversary, the K2 compiler deep dive, and the Compose Multiplatform Desktop 1.0 announcement. The full playlist is up on the JetBrains YouTube channel.
Android Developers Backstage — Episode on AGP 9.0: Xavier Ducrohet and Jerome Dochez walk through what changed in the build pipeline, why configuration cache is now mandatory, and what plugin authors need to update.
Haze — Chris Banes’ blur effect library for Compose. If you’ve tried to blur content behind a top bar or bottom sheet in Compose, you know the pain. Haze makes it a few lines:
@Composable
fun BlurredScaffold() {
val hazeState = remember { HazeState() }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Feed") },
modifier = Modifier.hazeChild(hazeState) {
progressive = HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f
)
}
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.haze(hazeState)
.padding(padding)
) {
items(feedItems) { item ->
FeedCard(item)
}
}
}
}
It uses RenderEffect on API 31+ for GPU-accelerated blur and falls back to a software implementation on older devices. No custom RenderNode hacks needed.
Accompanist is officially archived: Google has archived the Accompanist library. All modules (SystemUiController, Pager, FlowLayout, Permissions, etc.) have been either merged into Compose core or replaced by official APIs. If you still depend on Accompanist, migrate now — no further updates will ship.
accompanist-systemuicontroller → Use enableEdgeToEdge() from Activity 1.8+accompanist-pager → Use HorizontalPager / VerticalPager from Compose Foundationaccompanist-permissions → Use the official permissions API from Activity ComposeAGP 8.x reaches end-of-life: With AGP 9.0 stable, AGP 8.x enters maintenance mode. Google will ship critical security patches only through Q2 2026. Plan your migration.
Compose compiler: skip-group-if-unchanged optimization: A new compiler optimization that skips emitting entire groups if the inputs haven’t changed. Previous behavior always emitted the group and then checked for changes at the slot level. This reduces both memory allocation and slot table operations for static UI trees.
Foundation: new LazyLayout prefetch scheduler: The prefetch logic for LazyColumn and LazyRow was rewritten to use a frame-time-aware scheduler. Instead of prefetching a fixed number of items, it now estimates how many items it can prepare within the remaining frame budget. Should reduce jank on scroll start for large item lists.
Use snapshotFlow to bridge Compose state into Flow-based logic:
When you need to react to Compose state changes in a ViewModel or repository layer, snapshotFlow converts Compose state reads into a cold Flow:
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
var query by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.distinctUntilChanged()
.filter { it.length >= 2 }
.collect { searchText ->
viewModel.search(searchText)
}
}
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("Search...") }
)
}
This avoids the need to hoist the query into a StateFlow in the ViewModel just for debouncing. The snapshotFlow reads the Compose state directly and emits whenever it changes.
That’s a wrap for this week! See you in the next issue. 🐝
Understanding Compose 1.10 Performance Under the Hood: A solid breakdown of how the new compiler optimizations in Compose 1.10 reduce recomposition scope. The article traces through the Compose compiler’s new stable lambda detection — if a lambda only captures stable values, the compiler now auto-memoizes it. Worth reading if you’ve ever wondered why your LazyColumn item lambdas kept recomposing.
Testing Flows Like a Pro with Turbine 2.0: Cash App’s Turbine testing library hit 2.0 and this article walks through the new Turbine {} block syntax, awaitComplete() semantics, and how it handles StateFlow vs SharedFlow differently. If you’ve been testing flows with raw first() or toList(), this is a significant upgrade.
Spatial Computing on Android — Getting Started with XR Libraries: Google’s new XR Compose library lets you build spatial UI for headset-class devices. The article covers the SpatialPanel composable, spatial anchors, and how the XR runtime interacts with the standard Activity lifecycle.
Profiling Compose Apps with Macrobenchmark 1.4: A practical guide to catching jank in Compose screens using Macrobenchmark’s new ComposeMetrics integration. It shows how to capture recomposition counts per frame and correlate them with dropped frames in the trace viewer.
End-of-Year AndroidX Roundup — 47 Libraries Updated in December: A community-maintained tracker listing every AndroidX library that shipped a new version in December 2025. Useful as a cross-reference to make sure your dependency catalog is current.
The December ‘25 release brings Compose 1.10.0 stable and Material 3 1.4.0. This is one of the biggest Compose releases of the year — performance improvements, new APIs, and a real animation overhaul.
retain API — Keep Objects Across RecompositionsThe new retain API is the answer to “how do I keep something expensive alive without remember + key gymnastics.” It survives recompositions and configuration changes within the same composition scope:
@Composable
fun DocumentViewer(documentId: String) {
val pdfRenderer = retain(documentId) {
PdfRenderer(ParcelFileDescriptor.open(
File(cacheDir, "$documentId.pdf"),
ParcelFileDescriptor.MODE_READ_ONLY
))
}
val pageCount = pdfRenderer.pageCount
Text("Pages: $pageCount")
}
AnimatedContent TransitionsAnimatedContent now supports shared element keys, making hero transitions between states cleaner:
@Composable
fun ProductCard(product: Product, expanded: Boolean) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
}
) { isExpanded ->
if (isExpanded) {
ExpandedProductView(product)
} else {
CompactProductView(product)
}
}
}
visible Modifier (1.11.0-alpha01)Landed in the alpha channel — visible is a simpler alternative to AnimatedVisibility when you just want show/hide without animation:
@Composable
fun ToolbarActions(showExtra: Boolean) {
Row {
IconButton(onClick = { /* share */ }) { Icon(Icons.Default.Share, null) }
IconButton(
onClick = { /* settings */ },
modifier = Modifier.visible(showExtra)
) {
Icon(Icons.Default.Settings, null)
}
}
}
Compose 1.10.0 (Stable): Stable lambda auto-memoization, retain API, smoother AnimatedContent transitions, and across-the-board recomposition performance gains.
Material 3 Compose 1.4.0 (Stable): New SegmentedButton component, updated DatePicker visuals, and improved ModalBottomSheet edge-to-edge support.
Compose 1.11.0-alpha01: The visible modifier lands. Also includes early work on a new text layout engine.
Ink 1.0.0 (Stable): Google’s stylus and ink input library graduates to stable. Provides stroke rendering, pressure sensitivity, and low-latency ink paths for drawing and note-taking apps.
Turbine 2.0.0: Complete rewrite of Cash App’s Flow testing library. New block-based API, better error messages, and first-class StateFlow support.
Navigation 3 1.1.0-alpha01: Shared element transitions between NavEntry destinations.
XR Compose 1.0.0-alpha03: Spatial panels, 3D anchors, and gaze-based interaction primitives.
Macrobenchmark 1.4.0 (Stable): Compose-specific metrics, recomposition tracing per frame, and improved baseline profile generation.
Activity 1.12.1: Bug fix for OnBackPressedCallback not firing on certain Samsung devices.
Wear Compose 1.5.6: Bug fixes for SwipeToDismissBox gesture conflicts.
Ink 1.0 gives you a production-ready drawing surface with pressure-sensitive strokes:
@Composable
fun DrawingCanvas() {
val inkState = rememberInkState()
InkCanvas(
state = inkState,
modifier = Modifier.fillMaxSize(),
brush = InkBrush(
color = Color.Black,
size = 4.dp,
pressureSensitivity = 0.8f
)
) {
// Strokes are automatically captured
}
Button(onClick = { inkState.undo() }) {
Text("Undo")
}
}
Google shipped alpha releases of the XR Compose library — the first real attempt at spatial UI for Android headset devices. SpatialPanel lets you place Compose UI in 3D space:
@Composable
fun SpatialDashboard() {
SpatialPanel(
anchor = SpatialAnchor.headLocked(
distance = 1.5f,
offsetY = 0.2f
),
size = DpSize(400.dp, 300.dp)
) {
// Regular Compose UI placed in 3D space
Column {
Text("Dashboard", style = MaterialTheme.typography.headlineMedium)
StatsGrid(viewModel.stats)
}
}
}
The CPU profiler now integrates with Compose recomposition tracing. You can see exactly which composables recomposed in each frame, how long each recomposition took, and whether it was skipped. The new “Compose Trace” tab surfaces this without needing to add manual trace sections.
Turbine 2.0 — If you test Kotlin Flows (and you should), Turbine 2.0 is a must-upgrade. The new block-based syntax replaces the old test {} approach with clearer semantics:
@Test
fun `search emits loading then results`() = runTest {
val viewModel = SearchViewModel(fakeRepository)
viewModel.searchResults.test {
viewModel.search("kotlin")
assertEquals(SearchState.Loading, awaitItem())
val results = awaitItem()
assertIs<SearchState.Success>(results)
assertEquals(3, results.items.size)
cancelAndIgnoreRemainingEvents()
}
}
The big improvement: error messages now show you the full emission history when an assertion fails, not just “expected X got Y.” Makes debugging flaky flow tests much faster.
Move gap-buffer slot table into its own package: The Compose team moved the SlotTable and associated classes into a separate package — a step toward allowing a link-buffer-based composer implementation to land behind a feature flag. This hints at potential memory layout optimizations for the Compose runtime in 2026.
New text layout engine prototype: Early commits show work on a new text measurement and layout pipeline for Compose Text. The goal appears to be reducing the number of measure passes for multi-line text from 2 to 1. Still experimental but worth watching.
Zac Sweers’ Year-End AndroidX Tracker: Zac published a script that diffs the AndroidX artifact list between two dates, showing every library that shipped. Useful for teams that do quarterly dependency updates and want to see what they missed.
Skydoves’ Landscapist 2.5: Jaewoong Eum shipped Landscapist 2.5 with Compose 1.10 compatibility, a new CrossfadeImage animation, and reduced memory footprint for thumbnail loading. If you use Landscapist over Coil’s native Compose integration, this is worth updating.
Use Modifier.drawWithContent instead of Modifier.drawBehind when you need to control draw order:
@Composable
fun BadgedAvatar(imageUrl: String, badgeCount: Int) {
Box(
modifier = Modifier
.size(48.dp)
.drawWithContent {
drawContent() // draw the avatar first
if (badgeCount > 0) {
// draw badge on top
drawCircle(
color = Color.Red,
radius = 8.dp.toPx(),
center = Offset(size.width - 4.dp.toPx(), 4.dp.toPx())
)
}
}
) {
AsyncImage(model = imageUrl, contentDescription = "Avatar")
}
}
drawBehind only draws behind the content. drawWithContent gives you full control — draw before, after, or in between. Useful for overlays, badges, and custom decorations without adding extra composables to the tree.
That’s a wrap for this week! See you in the next issue. 🐝
Effective Compose UI Testing — What Actually Works: A practical walkthrough of the new ComposeTestRule improvements that landed this month. The key takeaway — onNodeWithText now supports regex matching out of the box, and the new assertIsDisplayed overloads let you assert visibility within a specific bounds window. If you’re doing Compose UI testing, this is the article to read.
Building Adaptive Layouts with Material 3 Window Size Classes: This one walks through using WindowSizeClass to build truly adaptive UIs that work across phones, foldables, and tablets without duplicating composables. The author uses a real e-commerce app as the example, which makes it practical.
DataStore Performance — Measuring the Real Cost: A benchmarking study comparing Proto DataStore vs Preferences DataStore vs SharedPreferences across cold reads, writes, and concurrent access. Proto DataStore wins on structured data by a wide margin — 3-4x faster reads on complex objects compared to Preferences DataStore with manual serialization.
Understanding Credentials API for Passkey Authentication: Google’s Credentials API simplifies passkey and password management. This article breaks down the CredentialManager flow and shows how to handle the full sign-in lifecycle including fallback to traditional passwords.
Compose Compiler Metrics — Reading Them Without Losing Your Mind: A guide to actually interpreting the stability reports the Compose compiler generates. The author explains the difference between “stable” and “immutable” in Compose’s world and why your data class might not be as stable as you think.
Droidcon London wrapped up this week and there were some genuinely great sessions. Here are the highlights worth tracking down:
Compose Performance — Beyond the Basics (Chris Banes): Chris walked through real performance traces from the Tivi app, showing how movableContentOf and the new retain API reduced recomposition counts by 40% on complex list screens. The key insight — most Compose performance issues aren’t about recomposition count, they’re about the work done during recomposition.
Scaling Design Systems Across 50+ Apps (Deliveroo): Deliveroo’s design system team shared how they built a token-based theme layer on top of Material 3 that serves 50+ feature modules. They use Compose’s CompositionLocal hierarchy for theming:
val LocalBrandTheme = staticCompositionLocalOf<BrandTheme> {
error("No BrandTheme provided")
}
@Composable
fun BrandThemeProvider(
brand: Brand,
content: @Composable () -> Unit
) {
val theme = remember(brand) { BrandTheme.from(brand) }
CompositionLocalProvider(
LocalBrandTheme provides theme
) {
MaterialTheme(
colorScheme = theme.colorScheme,
typography = theme.typography,
content = content
)
}
}
Testing Strategies That Actually Scale (Google): Manuel Vivo presented Google’s internal testing pyramid for Compose apps — the ratio that works for them is 70% screenshot tests, 20% integration tests, 10% end-to-end. Screenshot testing with Roborazzi was the clear winner for catching UI regressions.
Structured Concurrency Patterns for Android (Shreyas Patil): Shreyas showed three patterns for managing concurrent work in ViewModels, with a focus on avoiding the common mistake of launching unstructured coroutines inside viewModelScope.
Paging 4.0.0-alpha01: The first alpha of Paging 4 is here. The big change — PagingSource is now a suspend function instead of returning PagingSourceLoadResult. The new RemoteMediator API is significantly simpler too.
Compose BOM 2025.12.00: Updated BOM mapping to Compose UI 1.10.1, Compose Material3 1.4.1, and Compose Foundation 1.10.1. Mostly bug fixes and stability improvements.
Credentials API 1.5.0-beta01: Adds support for conditional UI — the credential picker now shows inline suggestions in the keyboard area on Android 14+.
DataStore 1.2.0: Performance improvements for Proto DataStore — cold read times reduced by roughly 30% in benchmarks. Also adds a new updateData overload that provides the current value for atomic read-modify-write operations.
Coil 3.1.0: Adds native AnimatedImageDecoder support and a new crossfade builder API. Memory caching got a rework — cache hits are now 15-20% faster.
OkHttp 4.12.1: Bug fix release addressing a connection pool leak under high concurrency. If you’re on 4.12.0, upgrade immediately.
The new PagingSource API is much cleaner:
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
val page = params.key ?: 1
return try {
val response = api.getArticles(page, params.loadSize)
LoadResult.Page(
data = response.articles,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.hasMore) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
The compose-ui-test module got a significant upgrade this cycle. The new SemanticsMatcher API lets you write more precise test assertions:
@Test
fun searchResults_showFilteredItems() {
composeTestRule.setContent {
SearchScreen(viewModel = testViewModel)
}
composeTestRule
.onNodeWithTag("search_input")
.performTextInput("kotlin")
composeTestRule
.onAllNodesWithTag("search_result")
.assertCountEquals(3)
.onFirst()
.assertTextContains("Kotlin", substring = true)
.assertHasClickAction()
}
The new waitUntilNodeCount API is also a game-changer for async UI testing — no more flaky advanceTimeBy calls:
composeTestRule.waitUntilNodeCount(
matcher = hasTestTag("list_item"),
count = 5,
timeoutMillis = 3000
)
The Credential Manager API now supports a streamlined flow for passkey registration:
suspend fun registerPasskey(context: Context) {
val credentialManager = CredentialManager.create(context)
val request = CreatePublicKeyCredentialRequest(
requestJson = fetchRegistrationOptionsFromServer()
)
val result = credentialManager.createCredential(
context = context,
request = request
)
when (result) {
is CreatePublicKeyCredentialResponse -> {
sendRegistrationToServer(result.registrationResponseJson)
}
}
}
Material 3 dynamic color got some nice improvements. The new dynamicColorScheme builder now supports custom seed colors as fallbacks when the user’s wallpaper-based palette isn’t available:
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicDarkColorScheme(context)
}
else -> {
darkColorScheme(
primary = BrandPurple,
secondary = BrandTeal,
tertiary = BrandAmber
)
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
You can now define custom color roles that participate in the tonal palette system:
@Immutable
data class ExtendedColors(
val success: Color,
val onSuccess: Color,
val successContainer: Color,
val warning: Color,
val onWarning: Color
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
success = Color.Unspecified,
onSuccess = Color.Unspecified,
successContainer = Color.Unspecified,
warning = Color.Unspecified,
onWarning = Color.Unspecified
)
}
Haze 1.2.0 (Chris Banes): The blur/glassmorphism library for Compose got a major update — real-time blur is now hardware-accelerated on Android 12+ using RenderEffect. Performance on older devices is improved too with a new fallback renderer.
Landscapist 2.5.0 (Skydoves): Adds built-in support for AnimatedImagePainter and a new placeholder shimmer effect that’s more customizable than previous versions. The API surface is also simplified — fewer required parameters for common use cases.
Roborazzi 1.35.0 — If you’re not doing screenshot testing yet, Roborazzi makes it incredibly easy to get started. It runs on JVM (no emulator needed), integrates with Robolectric, and generates pixel-perfect comparison reports. The setup is minimal:
// build.gradle.kts
plugins {
id("io.github.takahirom.roborazzi") version "1.35.0"
}
// In your test
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class ScreenshotTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun homeScreen_snapshot() {
composeTestRule.setContent {
AppTheme { HomeScreen() }
}
composeTestRule
.onRoot()
.captureRoboImage("snapshots/home_screen.png")
}
}
Use snapshotFlow to bridge Compose state into Flow for side effects:
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
var query by remember { mutableStateOf("") }
// ❌ Bad — triggers on every recomposition
LaunchedEffect(query) {
viewModel.search(query)
}
// ✅ Good — debounces and only emits when value changes
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.distinctUntilChanged()
.filter { it.length >= 2 }
.collect { viewModel.search(it) }
}
OutlinedTextField(
value = query,
onValueChange = { query = it },
label = { Text("Search") }
)
}
The snapshotFlow converts Compose state reads into a cold Flow, giving you access to all the flow operators for debouncing, filtering, and transforming state changes.
That’s a wrap for this week! See you in the next issue. 🐝
Android 16 Developer Preview 2 — What Developers Need to Know: The second developer preview drops new APIs for app-specific language preferences, improved per-app audio routing, and the new HealthConnect data types. The target SDK requirement bump to API 36 is confirmed for August 2026 — start planning now.
KSP 2.0 — Faster Annotation Processing for Kotlin: JetBrains shipped KSP 2.0 with a completely rewritten frontend that plugs directly into the K2 compiler. Build times for annotation-heavy modules drop 30-50% in their benchmarks. If you’re still on KAPT, this is the migration signal.
Compose Animation APIs — The Missing Guide: A walkthrough of the new AnimatedContent improvements and the SharedTransitionScope that landed in Compose 1.10. The author builds a music player transition as the example — album art morphing into a mini player — which makes the APIs click.
Room KMP — Building a Shared Database Layer: Room now runs on iOS, desktop, and Android from a single source set. This article covers the setup, the platform-specific RoomDatabase.Builder configuration, and the gotchas around thread confinement on iOS.
Privacy Sandbox on Android — Status Update: The Attribution Reporting API and Topics API are now in general availability. The article breaks down what this means for ad-supported apps and what migration steps are needed before third-party cookie deprecation.
Glance 2.0 for App Widgets — First Look: The Glance library for building widgets with Compose-like syntax got a major version bump. New lazy list support, improved theming, and better interactivity with action callbacks.
Android 16 DP2 brings a few APIs worth exploring early. Here are the highlights:
Apps can now request notification permissions at a per-channel level instead of the blanket POST_NOTIFICATIONS permission:
if (Build.VERSION.SDK_INT >= 36) {
val notificationManager = getSystemService<NotificationManager>()
val channel = NotificationChannel(
"order_updates",
"Order Updates",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setAllowBubbles(true)
}
notificationManager.createNotificationChannel(channel)
// Request permission for this specific channel
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
channelId = "order_updates"
)
}
The per-app language API now supports querying the user’s preferred language without the AppCompatDelegate workaround:
val localeManager = getSystemService<LocaleManager>()
// Get user's preferred locale for this app
val appLocale = localeManager.applicationLocales
// Set app locale programmatically
localeManager.applicationLocales = LocaleList.forLanguageTags("es")
The photo picker now returns results in the order the user selected them — a long-requested feature for apps that need ordered media selection.
KSP 2.0.0: Rewritten to use the K2 compiler frontend directly. Processing is 30-50% faster for annotation-heavy projects. Migration from KSP 1.x is mostly source-compatible — check the migration guide for edge cases around Resolver API changes.
Room 2.7.0-alpha08 (KMP): Room now supports Kotlin Multiplatform targets — iOS, JVM desktop, and Android from a single source set. The SQL dialect and migration system work identically across platforms.
Glance 2.0.0-alpha01: Major rewrite of the widget library. Lazy lists, improved action handling, and proper theming support that follows the device’s Material You colors.
Compose Animation 1.10.1: Bug fixes for SharedTransitionScope and AnimatedContent. The ExitTransition.None behavior is fixed — it was previously leaking composable nodes.
kotlinx.serialization 1.8.0-RC: Adds @SerialName support for enum entries and a new JsonNamingStrategy for automatic camelCase-to-snake_case conversion. Also improves error messages for malformed JSON.
Ktor 3.1.0: Adds built-in support for Server-Sent Events on the client side and improved WebSocket reconnection logic. The ContentNegotiation plugin now auto-detects kotlinx.serialization.
Setting up Room for multiplatform requires platform-specific database builders:
// commonMain — shared DAO and Entity definitions
@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY createdAt DESC")
fun getAllTasks(): Flow<List<Task>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(task: Task)
}
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val createdAt: Long = Clock.System.now().toEpochMilliseconds()
)
// androidMain
fun createAndroidDatabase(context: Context): AppDatabase {
return Room.databaseBuilder<AppDatabase>(
context = context,
name = context.getDatabasePath("app.db").absolutePath
).build()
}
// iosMain
fun createIosDatabase(): AppDatabase {
val dbPath = NSHomeDirectory() + "/Documents/app.db"
return Room.databaseBuilder<AppDatabase>(
name = dbPath
).build()
}
The Attribution Reporting API is now GA. If your app uses ad attribution, here’s the registration flow:
class AdAttributionManager(private val context: Context) {
private val measurementManager =
context.getSystemService<MeasurementManager>()
suspend fun registerAdClick(sourceUri: Uri) {
measurementManager?.registerSource(
attributionSource = sourceUri,
inputEvent = null // null for view-through, InputEvent for click
)
}
suspend fun registerConversion(triggerUri: Uri) {
measurementManager?.registerTrigger(triggerUri)
}
}
The Topics API provides coarse-grained interest signals without tracking individual users:
suspend fun getTopics(context: Context): List<Topic> {
val topicsManager = context.getSystemService<TopicsManager>()
val request = GetTopicsRequest.Builder()
.setAdsSdkName("com.example.ads")
.setShouldRecordObservation(true)
.build()
return topicsManager
?.getTopics(request)
?.topics
.orEmpty()
}
KSP 2.0 Migration Guide (JetBrains): A 30-minute walkthrough covering the K2 integration, what changed in the Resolver API, and how to migrate custom processors. The performance comparison at the end is convincing — 45% faster on the JetBrains Fleet codebase.
Compose Shared Element Transitions — A Production Story: A Droidcon talk showing how a travel app implemented shared element transitions between list and detail screens using SharedTransitionScope. The before/after performance numbers are solid — 60fps consistently on mid-range devices.
Privacy Sandbox Deep Dive (Google): Google’s privacy team walks through the Attribution Reporting and Topics APIs with production examples. The key message — start migrating now, third-party alternatives are being phased out.
KAPT → KSP Migration Deadline: With KSP 2.0 stable, JetBrains is officially deprecating KAPT. The library will receive only critical bug fixes going forward. If you’re using Dagger/Hilt, Room, or Moshi with KAPT — all three now have full KSP support. Migrate now:
// build.gradle.kts — Before (KAPT)
plugins {
kotlin("kapt")
}
dependencies {
kapt("com.google.dagger:hilt-compiler:2.52")
kapt("androidx.room:room-compiler:2.7.0")
}
// After (KSP)
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
ksp("com.google.dagger:hilt-compiler:2.52")
ksp("androidx.room:room-compiler:2.7.0")
}
AsyncTask Removal in Android 16: AsyncTask is fully removed from the Android 16 SDK. Apps still referencing it will get compile errors when targeting API 36. This has been deprecated since API 30, but if you have legacy code, now’s the time.
Sandwich 2.1.0 (Skydoves): The API response handling library adds built-in Ktor support alongside Retrofit. The new ApiResponse.suspendOnSuccess and suspendOnError extensions make it cleaner to chain async operations in the success/error branches.
Decompose 3.3.0 (Arkadii Ivanov): The Kotlin Multiplatform navigation/lifecycle library adds a new PredictiveBackGesture component for Android’s predictive back integration in shared KMP code.
Compose Hot Reload (JetBrains): The experimental Compose Hot Reload feature in the latest Android Studio Canary lets you see composable changes without restarting the app. It works for layout, color, and text changes — not logic changes yet. Enable it in Settings → Experimental → Compose Hot Reload. It’s rough around the edges but already useful for UI iteration.
Use collectAsStateWithLifecycle instead of collectAsState — always:
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
// ❌ Keeps collecting even when the app is in the background
val uiState by viewModel.uiState.collectAsState()
// ✅ Stops collecting when lifecycle drops below STARTED
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is ProfileState.Loading -> LoadingIndicator()
is ProfileState.Success -> {
val profile = (uiState as ProfileState.Success).profile
ProfileContent(
name = profile.name,
email = profile.email,
avatarUrl = profile.avatarUrl
)
}
is ProfileState.Error -> ErrorMessage(
message = (uiState as ProfileState.Error).message
)
}
}
This is the #1 most common Compose mistake. collectAsState doesn’t respect the lifecycle, which means your Flow keeps collecting (and potentially making network calls) even when the app is backgrounded. Always use the lifecycle-aware version from androidx.lifecycle:lifecycle-runtime-compose.
That’s a wrap for this week! See you in the next issue. 🐝
AGP 8.8 Stable — What Changed and Why You Should Upgrade: The Android Gradle Plugin 8.8 brings R8 full mode as default, improved build cache hit rates, and faster configuration phase. The R8 full mode change is the one to watch — it applies more aggressive optimizations but can break code that relies on reflection. Test your release builds carefully.
Type-Safe Navigation in Compose — The Complete Guide: With the navigation-compose library now supporting type-safe route definitions via Kotlin serialization, this article walks through migrating from string-based routes. The code is cleaner and the compile-time safety catches a whole class of bugs that used to crash at runtime.
Lifecycle 2.9 — What’s New Under the Hood: The new Lifecycle.launchOnResumed extension, improved SavedStateHandle support for Compose, and the deprecation of LifecycleObserver in favor of DefaultLifecycleObserver. The article digs into the internal refactoring that makes lifecycle observation cheaper.
CameraX 1.5.0 — Simplified Camera Setup: CameraX continues to simplify what used to be one of Android’s most painful APIs. The new CameraController.setImageAnalysisAnalyzer now supports frame rate throttling out of the box, which is huge for ML inference use cases.
Google Play’s New Data Deletion Requirements: Starting February 2026, all apps that allow account creation must provide an in-app and web-based path for users to request data deletion. The article covers the implementation requirements and the Play Console setup.
Building Compose Watch Faces for Wear OS 5: A hands-on guide to using the Compose-based Watch Face API that replaces the old WatchFaceService. The new API is dramatically simpler — what used to take 500+ lines now takes about 80.
R8 full mode is now the default for release builds. This means more aggressive optimizations — unused code removal, class merging, and enum unboxing. If you haven’t tested with full mode, do it now before upgrading:
// build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
// R8 full mode is now default — no need to set it
// To opt out temporarily (not recommended long-term):
// isCoreLibraryDesugaringEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
If you’re using reflection-heavy libraries, make sure your ProGuard rules are solid. Common things that break: Retrofit service interfaces, Moshi adapters, and Room entities. Add keep rules:
// proguard-rules.pro additions for R8 full mode
-keepclassmembers class * {
@com.squareup.moshi.Json <fields>;
}
-keep class * extends androidx.room.RoomDatabase
-keep @interface retrofit2.http.*
Configuration cache hit rates improved by roughly 20% for multi-module projects. The AGP now caches more task outputs by default, meaning incremental builds after a clean are noticeably faster — 15-25% in Google’s benchmarks on a 100-module project.
AGP 8.8.0: R8 full mode default, improved build cache, faster configuration phase. Minimum Gradle version bumped to 8.10.
Lifecycle 2.9.0: New launchOnResumed extension, SavedStateHandle Compose integration improvements, LifecycleObserver deprecated.
Navigation-Compose 2.9.0-alpha04: Full type-safe route support using Kotlin serialization. String-based routes are now officially discouraged.
CameraX 1.5.0: Frame rate throttling for image analysis, improved low-light capture, new ScreenFlashUiControl for front camera flash.
Wear Compose 1.5.1: Bug fixes for ScalingLazyColumn scroll position restoration and improved TimeText composable.
Hilt 2.53: Improved KSP processor performance — 20% faster on large projects. Also fixes a rare crash when using @AssistedInject with SavedStateHandle.
Turbine 1.2.0 (Cash App): New awaitComplete() and awaitError() extensions. The testIn scope now supports structured concurrency — tests fail properly when child coroutines throw.
The new type-safe navigation API replaces string routes with serializable objects:
@Serializable
data object HomeRoute
@Serializable
data class DetailRoute(val itemId: String)
@Serializable
data class ProfileRoute(val userId: Long, val tab: String = "overview")
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(
onItemClick = { itemId ->
navController.navigate(DetailRoute(itemId))
}
)
}
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(itemId = route.itemId)
}
composable<ProfileRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ProfileRoute>()
ProfileScreen(userId = route.userId, tab = route.tab)
}
}
}
No more "detail/{itemId}" strings and manual argument parsing. Type mismatches are caught at compile time.
Data Deletion API (Mandatory by February 2026): All apps with account creation must implement both an in-app deletion flow and a web URL for account/data deletion. Here’s the recommended implementation pattern:
class AccountDeletionViewModel(
private val accountRepository: AccountRepository,
private val authManager: AuthManager
) : ViewModel() {
sealed interface DeletionState {
data object Idle : DeletionState
data object Confirming : DeletionState
data object Deleting : DeletionState
data object Completed : DeletionState
data class Error(val message: String) : DeletionState
}
private val _state = MutableStateFlow<DeletionState>(DeletionState.Idle)
val state = _state.asStateFlow()
fun requestDeletion() {
_state.value = DeletionState.Confirming
}
fun confirmDeletion() {
viewModelScope.launch {
_state.value = DeletionState.Deleting
accountRepository.deleteAccount()
.onSuccess {
authManager.signOut()
_state.value = DeletionState.Completed
}
.onFailure { error ->
_state.value = DeletionState.Error(error.message ?: "Deletion failed")
}
}
}
}
The new frame rate throttling for image analysis is perfect for ML use cases where you don’t need 30fps of inference:
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetFrameRate(Range(5, 15)) // Throttle to 5-15 fps
.build()
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
val bitmap = imageProxy.toBitmap()
val results = mlModel.detect(bitmap)
updateOverlay(results)
imageProxy.close()
}
Wear OS Compose — Building for the Wrist (Google): A session covering Wear Compose 1.5 components — ScalingLazyColumn, TimeText, PositionIndicator, and the new RotaryScrollable modifier. The key takeaway is that Wear Compose now has near-parity with the phone Compose library.
Inside AGP 8.8 — Performance Deep Dive (Android Team): The Android build tools team walks through the configuration cache improvements and R8 full mode internals. They show real build traces from the Now in Android project — clean build dropped from 45s to 38s.
The State of Kotlin Multiplatform in Production (Touchlab): Touchlab shares data from 50+ KMP production apps — common module code sharing averages 60-70%, with networking and data layers being the most shared. The main pain point is still iOS tooling and debugging.
CVE-2025-XXXX — OkHttp Connection Pool Leak: A vulnerability in OkHttp 4.12.0 allows connection pool exhaustion under high concurrency, which can lead to denial-of-service conditions in apps making many parallel network requests. Fixed in OkHttp 4.12.1. Upgrade immediately if you’re on 4.12.0.
Play Protect Improvements: Google Play Protect now scans sideloaded apps against the on-device ML model in real time. Apps that request sensitive permissions (camera, microphone, location) at install time get additional scrutiny. No developer action needed, but expect more automated policy warnings in the Play Console.
LifecycleObserver → DefaultLifecycleObserver: The LifecycleObserver interface with annotation-based event handling (@OnLifecycleEvent) is now deprecated. Migrate to DefaultLifecycleObserver:
// ❌ Deprecated
class LocationObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTracking() { /* ... */ }
}
// ✅ Use this instead
class LocationObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
// Start location tracking
}
override fun onStop(owner: LifecycleOwner) {
// Stop location tracking
}
}
Circuit 1.0 Stable (Slack): Zac Sweers and the Slack team shipped Circuit 1.0 — the Compose-first architecture framework using Presenter and Ui as the core building blocks. It’s opinionated but the API surface is small and testable. Worth evaluating if you’re starting a new project.
Molecule 2.1.0 (Cash App): Minor update with improved error reporting when a MoleculeFlow throws during composition. Stack traces now point to the exact composable that failed.
Use remember with movableContentOf to keep expensive composables alive during layout changes:
@Composable
fun AdaptiveLayout(isExpanded: Boolean) {
val detailContent = remember {
movableContentOf {
// This composable keeps its state when moved
ExpensiveVideoPlayer(videoId = "abc123")
}
}
if (isExpanded) {
Row {
ListPane(modifier = Modifier.weight(0.4f))
detailContent() // Video player moves here without restart
}
} else {
Column {
ListPane()
detailContent() // Same instance, no re-initialization
}
}
}
When isExpanded changes, the ExpensiveVideoPlayer composable moves between the Row and Column layouts without losing its state or restarting playback. This is perfect for adaptive layouts on foldables.
That’s a wrap for this week! See you in the next issue. 🐝
KotlinConf Global 2025 — The Highlights That Matter: KotlinConf wrapped up with some major announcements — Kotlin 2.2 stable, Compose Multiplatform 1.8, and the new Kotlin Notebook plugin. The keynote focused on Kotlin’s trajectory as a multi-platform language, not just an Android language. The vibe was clear: JetBrains is betting hard on KMP.
Android Studio Koala Goes Stable: Koala (2024.2.2) is now the recommended stable release. The standout feature is the new Compose Preview Gallery mode that lets you see all your @Preview annotated composables in a grid view. Build analysis tooling also got a major upgrade with the new Build Analyzer 2.0.
Molecule 2.0 — What Changed and Why It Matters: Cash App shipped Molecule 2.0 with a simplified API, better error handling, and first-class support for StateFlow output. The launchMolecule function is gone — replaced by moleculeFlow which is more idiomatic.
Health Connect API — Complete Integration Guide: A practical guide covering the full Health Connect integration — permissions, data reading, data writing, and background sync. The author benchmarks the read performance across different data types and shares the gotchas around permission scoping.
AndroidX November Release Wave — Everything That Shipped: A roundup of every AndroidX library that got a new version in November. This was a big release month — 40+ libraries shipped updates including Activity 1.10, Fragment 1.8, WorkManager 2.10, and more.
Test Fixtures in Android — The Missing Guide: How to use Gradle’s testFixtures source set to share test utilities across modules without polluting your main source set. The article shows a real multi-module project setup.
KotlinConf was packed this year. Here are the sessions worth your time:
The State of Kotlin (Roman Elizarov): Roman laid out Kotlin’s roadmap — context parameters moving toward stable, the new kotlin-power-assert plugin becoming first-party, and Kotlin/Wasm reaching beta. The quote that stuck: “Kotlin is no longer just a better Java — it’s a platform.”
Compose Multiplatform 1.8 (Sebastian Aigner): CMP 1.8 brings parity with Jetpack Compose 1.10 — shared element transitions, lazy staggered grids, and improved text rendering on iOS. The iOS renderer now uses Skia by default with a Metal backend. Performance on iOS is within 5% of SwiftUI for most benchmarks.
Coroutines 2.0 — What’s Coming (Vsevolod Tolstopyatov): An overview of the upcoming coroutines 2.0 changes — improved Dispatchers.IO that auto-scales better, new Flow.chunked operator, and the experimental select overhaul. The Flow.chunked API is especially useful:
// Batch items from a flow into chunks for efficient processing
sensorDataFlow
.chunked(size = 50, timeout = 1.seconds)
.collect { batch ->
database.insertAll(batch) // Insert 50 records at once
}
Building Offline-First Apps with SQLDelight and Ktor (Touchlab): A practical session showing the offline-first pattern using SQLDelight as the source of truth and Ktor for sync. The architecture is clean — repository pattern with a SyncManager that handles conflict resolution:
class TaskRepository(
private val db: TaskDatabase,
private val api: TaskApi,
private val syncManager: SyncManager
) {
fun observeTasks(): Flow<List<Task>> =
db.taskQueries
.selectAll()
.asFlow()
.mapToList(Dispatchers.IO)
suspend fun addTask(task: Task) {
db.taskQueries.insert(task)
syncManager.enqueueSync(SyncAction.Upload(task.id))
}
suspend fun sync() {
val pending = db.syncQueries.getPending().executeAsList()
pending.forEach { action ->
api.pushTask(action.taskId)
.onSuccess { db.syncQueries.markSynced(action.id) }
.onFailure { db.syncQueries.incrementRetry(action.id) }
}
}
}
Android Studio Koala 2024.2.2 (Stable): Compose Preview Gallery, Build Analyzer 2.0, improved Logcat filters, and new App Quality Insights integration with Firebase Crashlytics.
Molecule 2.0.0 (Cash App): API redesign — moleculeFlow replaces launchMolecule, better error propagation, StateFlow output support.
Activity 1.10.0: New EdgeToEdge API improvements and enableEdgeToEdge() is now the recommended way to go edge-to-edge.
Fragment 1.8.0: strictMode API for catching common Fragment misuse in debug builds. Also deprecates setRetainInstance(true) with a proper migration path.
WorkManager 2.10.0: Adds ForegroundServiceType support for Android 14+ foreground service restrictions. New Constraints.Builder.setRequiresDeviceIdle() for truly background-only work.
Health Connect 1.1.0-beta01: New data types — blood glucose, nutrition, and hydration records. Also adds batch read support for more efficient data queries.
kotlinx-datetime 0.7.0: Adds DateTimePeriod.parse and improved Instant.toLocalDateTime performance. Also adds DayOfWeek utility extensions.
Paparazzi 1.4.0 (Cash App): Adds Compose Multiplatform screenshot testing support and improved layout rendering accuracy for Material 3 components.
The new API is cleaner and more idiomatic:
// Before (Molecule 1.x)
val scope = CoroutineScope(Dispatchers.Main)
val stateFlow = scope.launchMolecule(RecompositionMode.Immediate) {
ProfilePresenter(userId)
}
// After (Molecule 2.0)
val stateFlow: StateFlow<ProfileState> = moleculeFlow(RecompositionMode.Immediate) {
ProfilePresenter(userId)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ProfileState.Loading
)
Health Connect is maturing fast. The 1.1.0 beta adds batch reading which makes dashboard-style apps feasible without performance headaches:
class HealthDashboard(private val healthConnectClient: HealthConnectClient) {
suspend fun getWeeklySummary(): WeeklySummary {
val now = Instant.now()
val weekAgo = now.minus(7, ChronoUnit.DAYS)
val stepsRequest = ReadRecordsRequest(
recordType = StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(weekAgo, now)
)
val heartRateRequest = ReadRecordsRequest(
recordType = HeartRateRecord::class,
timeRangeFilter = TimeRangeFilter.between(weekAgo, now)
)
// Batch read — single round trip
val steps = healthConnectClient.readRecords(stepsRequest)
val heartRate = healthConnectClient.readRecords(heartRateRequest)
return WeeklySummary(
totalSteps = steps.records.sumOf { it.count },
avgHeartRate = heartRate.records
.flatMap { it.samples }
.map { it.beatsPerMinute }
.average()
)
}
}
Activity 1.10 makes edge-to-edge the standard. The new enableEdgeToEdge() call replaces the old WindowCompat.setDecorFitsSystemWindows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // Call before super.onCreate
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = ScaffoldDefaults.contentWindowInsets
) { innerPadding ->
AppContent(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
Gradle Build Scan + Build Analyzer 2.0: Android Studio Koala ships with a revamped Build Analyzer that finally gives actionable insights instead of generic warnings. The new “Critical Path” view shows exactly which tasks are blocking your build, and the Gradle Build Scan integration lets you share build performance data with your team:
// settings.gradle.kts — Enable build scans
plugins {
id("com.gradle.develocity") version "3.18"
}
develocity {
buildScan {
termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use")
termsOfUseAgree.set("yes")
publishing.onlyIf { !it.buildResult.failures.isEmpty() }
}
}
The “only publish on failure” configuration is the sweet spot — you get build scans for debugging without the noise of publishing every successful build.
Compose Compiler — Lambda Stability Optimization: A new optimization in the Compose compiler detects lambdas that capture only stable values and automatically memoizes them. Before this change, lambdas like onClick = { viewModel.doSomething() } would always be treated as unstable. Now the compiler checks whether viewModel is stable and skips recomposition if it is. This can reduce recomposition counts by 10-20% in real apps without any code changes.
Fragment StrictMode Internals: The new Fragment.StrictMode API in Fragment 1.8 works by intercepting Fragment lifecycle transitions and checking for common violations — wrong thread access, retained instance misuse, and target fragment usage. In debug builds it throws, in release it logs. A nice pattern if you want to build similar strict modes for your own components.
Haze 1.1.0 (Chris Banes): The glassmorphism/blur library adds HazeStyle.Companion.Elevated and HazeStyle.Companion.Thick presets for common blur effects. Also fixes a rendering issue on Samsung devices running One UI 6.
Showkase 1.1.0 (Airbnb): Airbnb’s component browser for Compose now supports dark mode previews, RTL layout previews, and font scaling previews — all auto-generated from your existing @Preview annotations.
Power-Assert Compiler Plugin (Kotlin): JetBrains made the kotlin-power-assert plugin first-party in Kotlin 2.2. It transforms assertion failures into detailed diagrams showing the exact value of each sub-expression:
// With power-assert enabled
val user = User(name = "Alice", age = 25)
assert(user.age >= 30)
// Output:
// assert(user.age >= 30)
// | | |
// | 25 false
// User(name=Alice, age=25)
Android Security Bulletin — November 2025: The November patch addresses 12 vulnerabilities in the Android framework, including a high-severity issue in the Media framework (CVE-2025-XXXXX) that could allow remote code execution via a crafted video file. The patch also includes fixes for Bluetooth stack vulnerabilities. If you target Android 14+, the BLUETOOTH_CONNECT permission now requires runtime approval even for previously-granted apps after this patch.
Use Gradle test fixtures to share test utilities across modules:
// build.gradle.kts — Enable test fixtures
plugins {
id("java-test-fixtures")
}
// src/testFixtures/kotlin/com/example/TestFactory.kt
object UserFactory {
fun create(
id: Long = 1L,
name: String = "Test User",
email: String = "test@example.com"
) = User(id = id, name = name, email = email)
fun createList(count: Int = 5) =
(1..count).map { create(id = it.toLong(), name = "User $it") }
}
// In another module's test
// build.gradle.kts
dependencies {
testImplementation(testFixtures(project(":core:model")))
}
// UserRepositoryTest.kt
class UserRepositoryTest {
@Test
fun `returns cached users`() = runTest {
val users = UserFactory.createList(3)
fakeDao.insertAll(users)
val result = repository.getUsers().first()
assertEquals(3, result.size)
}
}
Test fixtures live in src/testFixtures/ and are automatically available to any module that declares testImplementation(testFixtures(...)). This keeps your test helpers reusable without polluting the main source set or duplicating factory code across modules.
That’s a wrap for this week! See you in the next issue. 🐝
Kotlin 2.1.20 — Bug Fixes and K2 Improvements: JetBrains shipped Kotlin 2.1.20 with K2 compiler fixes for edge cases in smart casting, improved error messages for context parameter misuse, and better IDE performance. The K2 frontend’s type resolution is noticeably snappier in Android Studio after this update.
Compose 1.9 — The Stability Release: Compose 1.9 focuses on polish — shared element transitions move from RC to stable, LazyColumn performance improvements land, and the new Modifier.Node migration for internal modifiers reduces memory allocation during recomposition by 15%. This is the release to stabilize on for production apps.
AndroidX October Release Roundup — Everything That Shipped: October was a big month for AndroidX. 45+ libraries shipped updates including Room 2.7, Paging 3.4, Lifecycle 2.9, Navigation-Compose 2.9, WorkManager 2.10, and DataStore 1.1.2. This article summarizes every notable change.
Coil 3.1 — Shared Elements and Cache Control: Coil 3.1 adds first-party support for Compose shared element transitions in AsyncImage, new MemoryCachePolicy and DiskCachePolicy for fine-grained control, and improved placeholder composable API. If you use Compose with image loading, Coil 3.1 is a meaningful upgrade.
Compose Modifier.Node — The Migration You Should Start Now: Compose is migrating internal modifiers from the composed pattern to Modifier.Node. The new approach allocates less, is faster during recomposition, and produces clearer stack traces. While this is mainly an internal change, custom modifier authors should start migrating.
Building a Design System with Compose — Lessons from 2 Years: An engineering team shares their journey building a design system on Compose — the token architecture, component versioning strategy, theme customization, and the testing infrastructure that catches visual regressions across 200+ components.
Shared element transitions are now stable after spending 3 releases in alpha/beta/RC:
@Composable
fun AppContent() {
SharedTransitionLayout {
val navController = rememberNavController()
NavHost(navController, startDestination = "feed") {
composable("feed") {
FeedScreen(
onItemClick = { item ->
navController.navigate("detail/${item.id}")
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
val id = it.arguments?.getString("id") ?: return@composable
DetailScreen(
itemId = id,
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
}
The Modifier.Node migration reduces memory allocation during recomposition. Custom modifiers should migrate from composed to Modifier.Node:
// ❌ Old pattern — allocates on every recomposition
fun Modifier.shimmer(): Modifier = composed {
val transition = rememberInfiniteTransition()
val alpha by transition.animateFloat(
initialValue = 0.3f,
targetValue = 1f,
animationSpec = infiniteRepeatable(tween(1000))
)
this.alpha(alpha)
}
// ✅ New pattern — Modifier.Node, allocates once
class ShimmerNode : Modifier.Node(), DrawModifierNode {
private var alpha = 0.3f
override fun ContentDrawScope.draw() {
drawContent()
// Shimmer logic with minimal allocations
}
}
fun Modifier.shimmer(): Modifier =
this then ShimmerElement()
private class ShimmerElement : ModifierNodeElement<ShimmerNode>() {
override fun create() = ShimmerNode()
override fun update(node: ShimmerNode) {}
override fun hashCode() = "shimmer".hashCode()
override fun equals(other: Any?) = other is ShimmerElement
}
Kotlin 2.1.20: K2 smart casting fixes, improved context parameter error messages, IDE performance improvements, minor stdlib additions.
Compose BOM 2025.11.03: Aligns Compose UI 1.9.0 (stable), Material 3 1.4.0-beta01, Foundation 1.9.0, Compose Compiler 2.1.3.
Coil 3.1.0: Shared element transition support for AsyncImage, MemoryCachePolicy and DiskCachePolicy fine-grained control, improved placeholder composable, better error handling for network failures.
Room 2.7.1: Bug fix for KSP incremental processing failing on TypeConverter changes. Also fixes a migration issue with @Upsert columns.
Navigation-Compose 2.9.0: Stable release — type-safe routes with Kotlin serialization, shared element transition integration, improved deep link handling.
Lifecycle 2.9.1: Patch release fixing a race condition in LifecycleResumeEffect cleanup when rapidly navigating between screens.
Paging 3.4.1: Bug fix for RemoteMediator refresh not triggering when using cachedIn with SharingStarted.Lazily.
kotlinx.coroutines 1.10.2: Fixes a thread leak on Dispatchers.IO.limitedParallelism when coroutines are cancelled during dispatch. Also improves Flow.stateIn initial value handling.
Haze 1.3.1 (Chris Banes): Performance optimization for progressive blur on devices with Adreno GPUs. Also fixes a crash on API 28 devices.
Coil’s AsyncImage now integrates seamlessly with Compose’s SharedTransitionLayout:
@Composable
fun SharedTransitionScope.ItemCard(
item: Item,
animatedVisibilityScope: AnimatedVisibilityScope,
onClick: () -> Unit
) {
Card(onClick = onClick) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(item.imageUrl)
.crossfade(true)
.build(),
contentDescription = item.title,
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope
)
.fillMaxWidth()
.height(200.dp),
contentScale = ContentScale.Crop
)
}
}
October was the biggest release month of 2025 for AndroidX. Here are the highlights beyond what we’ve already covered in previous issues:
Activity 1.10.1: Fixes an edge case where enableEdgeToEdge() didn’t apply correctly on Samsung devices running One UI 6.1.
Browser 1.9.0: Custom Tabs now support partial height — you can show a browser sheet that covers only part of the screen, perfect for in-app link previews.
Core Splashscreen 1.2.0: Better support for animated vector drawables in splash screens. Also fixes a timing issue where the splash screen dismissed too early on slow devices.
Media3 1.5.0: ExoPlayer improvements — better adaptive streaming, reduced buffer memory usage, and new MediaSession callbacks for background playback control.
The partial-height Custom Tab is useful for previewing links without leaving the app:
fun openLinkPreview(context: Context, url: String) {
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(ContextCompat.getColor(context, R.color.surface))
.build()
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(colorSchemeParams)
.setInitialActivityHeightPx(800) // Partial height
.setCloseButtonPosition(CustomTabsIntent.CLOSE_BUTTON_POSITION_END)
.setToolbarCornerRadiusDp(16)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url))
}
Compose Foundation — Text Hyphenation: New commits add automatic text hyphenation support to Compose’s Text composable. The implementation uses Android’s platform hyphenator and adds a Hyphenation parameter. This is useful for narrow text containers where long words cause awkward line breaks.
Material 3 — Predictive Back for Dialogs: Material 3 dialogs now support predictive back — swiping back on a dialog shows a preview animation of the dialog dismissing. This matches the system behavior and feels natural.
Dependency Guard (Dropbox): A Gradle plugin that creates a snapshot of your dependency tree and fails the build if it changes unexpectedly. This prevents accidental dependency upgrades that could introduce breaking changes or security vulnerabilities:
// build.gradle.kts
plugins {
id("com.dropbox.dependency-guard") version "0.5.0"
}
dependencyGuard {
configuration("releaseRuntimeClasspath") {
// Generate baseline: ./gradlew dependencyGuardBaseline
// Check: ./gradlew dependencyGuard
tree = true // Include transitive dependencies
}
}
Run ./gradlew dependencyGuardBaseline to generate the snapshot, then ./gradlew dependencyGuard on CI to catch unexpected changes. It’s caught real issues — transitive upgrades that bumped minimum API levels, libraries that pulled in conflicting versions of OkHttp, and debug-only dependencies leaking into release builds.
Landscapist 2.5.1 (Skydoves): Jaewoong Eum released a Compose 1.9 compatibility update with improved shared element transition support and a new rememberImagePainter API that works better with SharedTransitionLayout.
kotlin-result 2.0.0 (Michael Bull): The popular Result type library for Kotlin ships 2.0 with a cleaner API, better KMP support, and new binding DSL for chaining operations. It’s an alternative to Arrow’s Either with a simpler API surface.
Compose Multiplatform Template (JetBrains): JetBrains updated the official CMP template with Kotlin 2.1, Compose 1.9, and a clean project structure that supports Android, iOS, Desktop, and Web targets out of the box.
Use movableContentOf with SharedTransitionLayout for complex adaptive layouts:
@Composable
fun AdaptiveDetailLayout(
windowSizeClass: WindowSizeClass,
item: Item
) {
val videoPlayer = remember {
movableContentOf {
VideoPlayer(
videoUrl = item.videoUrl,
modifier = Modifier.fillMaxWidth()
)
}
}
val comments = remember {
movableContentOf {
CommentsList(itemId = item.id)
}
}
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
// Phone — vertical layout
Column {
videoPlayer()
comments()
}
}
else -> {
// Tablet/Desktop — side by side
Row {
Box(modifier = Modifier.weight(0.6f)) { videoPlayer() }
Box(modifier = Modifier.weight(0.4f)) { comments() }
}
}
}
}
movableContentOf ensures the VideoPlayer keeps its internal state (playback position, buffered data) when moving between layouts. Without it, switching from phone to tablet layout would restart the video.
Compose’s LaunchedEffect with Unit as the key runs exactly once when the composable enters composition — but it’s not the same as init in a class. If the parent composable recomposes and the LaunchedEffect’s host composable leaves and re-enters composition (e.g., inside an if block), the effect runs again:
@Composable
fun ParentScreen() {
var showChild by remember { mutableStateOf(true) }
Button(onClick = { showChild = !showChild }) {
Text("Toggle")
}
if (showChild) {
ChildScreen() // LaunchedEffect runs AGAIN every time this re-enters
}
}
@Composable
fun ChildScreen() {
// Runs every time ChildScreen enters composition
// Toggle off → toggle on = runs again
LaunchedEffect(Unit) {
analytics.trackScreenView("child")
// This logs EVERY time the user toggles showChild to true
}
}
If you need truly once-per-session behavior, track it in a ViewModel or a persistent state holder — not in LaunchedEffect(Unit).
Q1: What does the Modifier.Node migration in Compose 1.9 improve?
A: Modifier.Node reduces memory allocation during recomposition by 15% compared to the old composed pattern. Modifier.Node instances are created once and reused, while composed modifiers create new instances on every recomposition.
Q2: What new feature does Coil 3.1 add for Compose navigation?
A: Coil 3.1 adds first-party support for shared element transitions in AsyncImage. The AsyncImage composable can now be used directly with sharedElement and sharedBounds modifiers inside a SharedTransitionLayout.
Q3: When does LaunchedEffect(Unit) run again in Compose?
A: It runs every time the composable enters the composition. If the composable leaves and re-enters composition (e.g., toggled by an if block), the effect runs again. It’s not a true “run once” — it’s “run once per composition entry.”
That’s a wrap for this week! See you in the next issue. 🐝
Droidcon NYC 2025 — The Talks Worth Your Time: Droidcon NYC wrapped up with a strong lineup. The standout sessions covered Circuit’s architecture patterns, Compose performance in production, and a surprisingly practical talk on Gradle build optimization that shaved 40% off a 200-module project’s build time.
Hilt 2.53 — KSP Performance and AssistedInject Fixes: Hilt 2.53 focuses on KSP processor performance — 20% faster annotation processing on large projects. The release also fixes a rare crash when combining @AssistedInject with SavedStateHandle and adds better error messages for common setup mistakes.
AGP 8.8 Preview — R8 Full Mode and Build Cache: The next AGP release is in preview with R8 full mode as the default, improved build cache hit rates, and faster configuration phase. The R8 full mode change is the one to watch — it applies more aggressive optimizations but can break reflection-heavy code.
Circuit 1.0 Stable — Slack’s Compose Architecture: Zac Sweers and the Slack team shipped Circuit 1.0 after two years of development. Circuit is a Compose-first architecture framework where Presenter handles business logic and Ui handles rendering — no ViewModel needed. The API surface is intentionally small.
Rethinking Android Architecture — ViewModels Are Not Mandatory: An opinionated article arguing that ViewModels add unnecessary complexity for simple screens. The author compares ViewModel-based, Molecule-based, and Circuit-based architectures with code examples and testing stories from production.
Gradle Build Optimization — The 80/20 Guide: The 5 changes that deliver 80% of build speed improvements: enable configuration cache, use KSP over KAPT, enable parallel execution, use build cache, and set appropriate JVM heap size. Each change is benchmarked on a real project.
Circuit in Production — 2 Years at Slack (Zac Sweers): Zac walked through Circuit’s evolution from internal experiment to stable 1.0. Key insight: separating Presenter from Ui made testing trivially easy — presenters are pure functions that return state, and the entire business logic layer has zero Android dependencies.
// Circuit — Presenter and Ui are separate, testable units
class ProfilePresenter @Inject constructor(
private val userRepository: UserRepository,
private val navigator: Navigator
) : Presenter<ProfileState> {
@Composable
override fun present(): ProfileState {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) {
user = userRepository.getCurrentUser()
}
return when (val u = user) {
null -> ProfileState.Loading
else -> ProfileState.Success(
user = u,
onLogout = { navigator.goTo(LoginScreen) }
)
}
}
}
// Test — no Activity, no ViewModel, no Android framework
@Test
fun `presents user data`() = runTest {
val presenter = ProfilePresenter(
userRepository = FakeUserRepository(testUser),
navigator = FakeNavigator()
)
moleculeFlow(RecompositionMode.Immediate) { presenter.present() }
.test {
assertEquals(ProfileState.Loading, awaitItem())
val success = awaitItem()
assertIs<ProfileState.Success>(success)
assertEquals("Alice", success.user.name)
}
}
Compose Performance — What We Learned at Scale (Google): The Android team shared real performance data from Google apps using Compose in production. The top findings: stability annotations are overused (most have no measurable impact), LazyColumn key assignment is underused (biggest single optimization), and baseline profiles improve cold start by 15-30%.
Gradle Build Times — From 8 Minutes to 2 (Square): Square’s build tools team shared their journey reducing build times on a 300-module project. The biggest wins: migrating from KAPT to KSP (saved 90 seconds), enabling configuration cache (saved 45 seconds), and restructuring module dependencies to reduce the critical path.
Circuit handles navigation through typed screens and a navigator:
// Define screens as data objects/classes
@Parcelize
data object HomeScreen : Screen
@Parcelize
data class DetailScreen(val itemId: String) : Screen
// Wire up navigation
@Composable
fun AppNavigation() {
val backStack = rememberSaveableBackStack(HomeScreen)
val navigator = rememberCircuitNavigator(backStack)
NavigableCircuitContent(
navigator = navigator,
backStack = backStack
)
}
Circuit treats overlays as first-class citizens with their own presenters:
class ConfirmDeleteOverlay : Overlay<ConfirmDeleteOverlay.Result> {
enum class Result { Confirmed, Cancelled }
@Composable
override fun Content(navigator: OverlayNavigator<Result>) {
AlertDialog(
onDismissRequest = { navigator.finish(Result.Cancelled) },
title = { Text("Delete item?") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(onClick = { navigator.finish(Result.Confirmed) }) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = { navigator.finish(Result.Cancelled) }) {
Text("Cancel")
}
}
)
}
}
// In presenter — show overlay and await result
@Composable
override fun present(): ItemState {
val overlayHost = LocalOverlayHost.current
val scope = rememberCoroutineScope()
return ItemState(
onDelete = {
scope.launch {
val result = overlayHost.show(ConfirmDeleteOverlay())
if (result == ConfirmDeleteOverlay.Result.Confirmed) {
repository.deleteItem(itemId)
navigator.pop()
}
}
}
)
}
Circuit 1.0.0 (Slack): Stable release — Presenter, Ui, Screen, Overlay, Navigator APIs finalized. KSP code generation for wiring presenters to screens.
Hilt 2.53.0: 20% faster KSP processing, @AssistedInject + SavedStateHandle fix, improved error diagnostics, Kotlin 2.1 compatibility.
AGP 8.8.0-beta01: R8 full mode as default, improved build cache, faster configuration phase. Minimum Gradle version bumped to 8.10.
Compose BOM 2025.11.02: Aligns Compose UI 1.8.0-rc01, Material 3 1.4.0-alpha05. Compose UI 1.8 hits release candidate — shared element transitions are stable.
Turbine 1.2.0 (Cash App): New awaitComplete() and awaitError() extensions, structured concurrency support for testIn scope.
Molecule 2.0.0 (Cash App): API redesign — moleculeFlow replaces launchMolecule, better error propagation, StateFlow output support.
Sandwich 2.1.0 (Skydoves): New suspendOnSuccess and suspendOnError operators for cleaner async response handling. Also adds Flow<ApiResponse> support.
Hilt 2.53’s KSP performance improvement is significant for large projects:
// build.gradle.kts — Hilt with KSP (recommended)
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
id("dagger.hilt.android.plugin")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.53")
ksp("com.google.dagger:hilt-compiler:2.53")
// For ViewModel injection
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}
The build cache improvements in AGP 8.8 are measurable. Configuration cache hit rates improved by 20% for multi-module projects, and incremental builds after clean are 15-25% faster:
// settings.gradle.kts — Optimize build configuration
plugins {
id("com.android.application") version "8.8.0-beta01" apply false
}
// gradle.properties — Maximum build performance
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
kotlin.incremental.useClasspathSnapshot=true
Compose UI 1.8 hits release candidate. Shared element transitions are now stable and the performance improvements are confirmed — 12% reduction in frame rendering time for complex layouts.
CatchUp 2.0 (Zac Sweers): Zac’s news aggregator app got a major rewrite using Circuit 1.0, Kotlin 2.1, and the latest Compose APIs. It serves as a reference project for modern Android architecture — well worth studying.
Haze 1.2.1 (Chris Banes): Patch release fixing a rendering issue on Pixel 9 devices running Android 15. Also improved blur performance on devices with Mali GPUs.
kotlin-inject 0.7.3: A lightweight compile-time dependency injection library. This release adds support for KMP targets and @Assisted injection — positioning itself as an alternative to Hilt for multiplatform projects.
Android Security Bulletin — November 2025: The November patch addresses 12 vulnerabilities, including a high-severity issue in the Media framework (CVE-2025-XXXXX) that could allow remote code execution via a crafted video file. The patch also includes fixes for Bluetooth stack vulnerabilities. Update your security patch level to 2025-11-05 or later.
Use produceState to convert suspend functions into Compose state cleanly:
// ❌ Manual state management with LaunchedEffect
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(userId) {
isLoading = true
user = repository.getUser(userId)
isLoading = false
}
if (isLoading) LoadingSpinner() else user?.let { ProfileContent(it) }
}
// ✅ produceState — cleaner, handles initial state
@Composable
fun UserProfile(userId: String) {
val userState by produceState<UiState<User>>(
initialValue = UiState.Loading,
key1 = userId
) {
value = try {
UiState.Success(repository.getUser(userId))
} catch (e: Exception) {
UiState.Error(e.message ?: "Failed to load")
}
}
when (val state = userState) {
is UiState.Loading -> LoadingSpinner()
is UiState.Success -> ProfileContent(state.data)
is UiState.Error -> ErrorMessage(state.message)
}
}
produceState launches a coroutine scoped to the composable’s lifecycle and provides a MutableState you can update from the coroutine. It’s the cleanest way to load data directly in a composable without a ViewModel.
Kotlin’s by lazy with the default LazyThreadSafetyMode.SYNCHRONIZED uses a lock for thread safety, which means accessing the value from multiple coroutines on different dispatchers incurs lock contention. For coroutine-heavy code, this can be a bottleneck:
// Default — uses synchronized lock (can block coroutines)
val expensiveConfig by lazy {
loadConfig() // If this is slow, other coroutines block waiting
}
// For coroutine-safe lazy initialization, use a suspend function:
class AppModule {
private val _config = kotlinx.coroutines.sync.Mutex()
private var _configValue: Config? = null
suspend fun getConfig(): Config {
_configValue?.let { return it }
return _config.withLock {
_configValue ?: loadConfig().also { _configValue = it }
}
}
}
// Or if you know access is single-threaded (e.g., Main dispatcher only):
val config by lazy(LazyThreadSafetyMode.NONE) {
loadConfig() // No synchronization overhead
}
The LazyThreadSafetyMode.NONE option skips synchronization entirely — 2-3x faster access but only safe when you guarantee single-threaded access.
Q1: What is Circuit’s core architectural concept?
A: Circuit separates business logic (Presenter) from rendering (Ui). Presenters are @Composable functions that return state objects, and Ui components render that state. This separation makes presenters trivially testable — no Android framework dependencies needed.
Q2: What’s the recommended approach for Hilt annotation processing in 2025?
A: Use KSP (ksp("com.google.dagger:hilt-compiler:2.53")) instead of KAPT. KSP is significantly faster — Hilt 2.53 reports 20% faster processing with KSP. KAPT is deprecated and will be removed in future Kotlin versions.
Q3: What does produceState do in Compose?
A: It launches a coroutine scoped to the composable’s lifecycle and provides a MutableState that the coroutine can update. It’s a clean way to load async data directly in a composable, handling initial state, lifecycle scoping, and state updates in one API.
That’s a wrap for this week! See you in the next issue. 🐝
Android 15 QPR2 Beta 1 — What’s in the Quarterly Platform Release: The QPR2 beta brings refinements to the predictive back gesture, improved notification permission handling, and a new HealthConnect data type for body temperature. QPR releases don’t add new API levels but they do change platform behavior — test your apps.
SQLDelight 2.1 — Async Drivers and Better Migrations: Cash App shipped SQLDelight 2.1 with async database drivers for iOS (using NSOperation under the hood), improved migration testing tooling, and a new .sq file syntax for window functions. The async driver solves the biggest pain point for KMP apps — blocking the main thread on iOS during database operations.
Compose Performance — 10 Patterns That Actually Matter: A benchmark-driven article testing 10 common Compose optimization patterns. The results are surprising — key in LazyColumn had the biggest impact (40% fewer recompositions), followed by contentType (25%), while @Stable annotations on already-stable classes had zero measurable effect.
Baseline Profiles — Measuring the Real Impact: A team measured baseline profile impact across 3 production apps. Cold start improved 18-32%, first frame render improved 12-20%, and scroll jank dropped by 15%. The article shares the measurement methodology and common pitfalls.
Understanding Compose Compiler Reports: A guide to reading and acting on Compose compiler reports — composables.txt, classes.txt, and the stability analysis. The article shows how to generate reports, interpret stability classifications, and fix the most common instability sources.
Kotlin Sequences vs Flows — When to Use What: A clear comparison showing that sequences are for synchronous in-memory transformations while flows are for asynchronous streams. The article benchmarks both and shows that sequences outperform flows by 3-5x for simple in-memory transformations.
The async driver is a game-changer for iOS targets where blocking the main thread causes watchdog kills:
// shared/src/commonMain/kotlin
expect class DriverFactory {
fun createDriver(): SqlDriver
}
// shared/src/iosMain/kotlin
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(
schema = AppDatabase.Schema,
name = "app.db",
// New in 2.1: async operations on iOS
maxReaderConnections = 4
)
}
}
// Queries now support suspend functions natively
class TaskRepository(private val db: AppDatabase) {
suspend fun getAllTasks(): List<Task> =
db.taskQueries.selectAll().awaitAsList()
fun observeTasks(): Flow<List<Task>> =
db.taskQueries.selectAll().asFlow().mapToList(Dispatchers.Default)
suspend fun insertTask(task: Task) {
db.taskQueries.insert(
id = task.id,
title = task.title,
completed = task.completed
)
}
}
SQLDelight 2.1 adds support for window functions in .sq files:
-- task.sq
selectTasksWithRank:
SELECT
id,
title,
priority,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY priority DESC) AS rank
FROM tasks
WHERE completed = 0;
selectRunningTotal:
SELECT
id,
amount,
SUM(amount) OVER (ORDER BY created_at) AS running_total
FROM transactions
WHERE user_id = ?;
SQLDelight 2.1.0 (Cash App): Async drivers for iOS/Native, window function support in .sq files, improved migration testing, better Kotlin 2.1 compatibility.
Android 15 QPR2 Beta 1: Predictive back refinements, improved notification handling, new HealthConnect data types, bug fixes for edge-to-edge rendering.
Compose BOM 2025.11.01: Aligns Compose UI 1.8.0-beta02, Material 3 1.4.0-alpha04. Performance improvements for LazyColumn item animations. Shared element transitions are more stable.
Retrofit 2.12.1 (Square): Bug fix for @Tag annotation not being forwarded to OkHttp interceptors. Also improved error messages for missing @Body on POST requests.
kotlinx.serialization 1.7.4: Fixes a deserialization bug with @Polymorphic types in JSON arrays. Also improved error messages for missing discriminator fields.
Napier 2.8.0: The KMP logging library adds structured logging with key-value pairs. Also supports log filtering by tag pattern — useful for large KMP projects with many modules.
Paparazzi 1.4.1 (Cash App): Fixed rendering issues with Material 3 components on API 34. Also improved Compose snapshot test speed by 20%.
Generate and analyze stability reports to find performance bottlenecks:
// build.gradle.kts — Enable compiler reports
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose-reports")
metricsDestination = layout.buildDirectory.dir("compose-metrics")
}
// Run: ./gradlew :app:assembleRelease
// Check: app/build/compose-reports/app_release-classes.txt
The classes.txt file shows stability analysis for every class used in composables:
// Example output — spot the unstable class
stable class TaskState {
stable val id: String
stable val title: String
stable val completed: Boolean
}
unstable class FeedState { // ← This causes recompositions
unstable val items: List<FeedItem> // List is unstable
stable val isLoading: Boolean
}
The predictive back gesture is refined in QPR2. The cross-activity animation is smoother and apps can now customize the animation curve:
class DetailActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Custom predictive back animation
onBackPressedDispatcher.addCallback(this) {
// Handle back with custom transition
handleOnBackPressed()
}
// For Compose-based back handling
setContent {
var showDetail by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = showDetail,
enter = slideInHorizontally { it },
exit = slideOutHorizontally { it }
) {
DetailContent(
onBack = { showDetail = false }
)
}
}
}
}
You can verify baseline profiles are being applied using ProfileVerifier:
class StartupProfileVerifier(context: Context) {
suspend fun checkProfileStatus(): ProfileStatus {
val result = ProfileVerifier.getCompilationStatusAsync()
.await()
return when (result.profileInstallResultCode) {
ProfileVerifier.CompilationStatus
.RESULT_CODE_COMPILED_WITH_PROFILE ->
ProfileStatus.Applied(result.isCompiledWithProfile)
ProfileVerifier.CompilationStatus
.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
ProfileStatus.Pending
else -> ProfileStatus.NotApplied
}
}
}
LazyColumn Prefetch Improvements: A series of commits in the Compose foundation module improve the LazyColumn prefetch algorithm. The new approach uses scroll velocity prediction — if the user is scrolling fast, it prefetches more items ahead. This reduces blank frames during fast scrolling by 30-40% in benchmarks.
Compose Compiler — Stability for External Types: New commits show work on allowing developers to configure stability for external types (types from libraries you don’t control). This would let you tell the Compose compiler “treat kotlinx.datetime.Instant as stable” without needing a wrapper class.
Maestro (Mobile.dev): A declarative UI testing framework that uses YAML to define test flows. No more flaky Espresso tests or complex setup — Maestro runs on real devices and emulators, handles animations and transitions automatically, and the tests read like user stories:
# flow/login-flow.yaml
appId: com.example.myapp
---
- launchApp
- tapOn: "Email"
- inputText: "test@example.com"
- tapOn: "Password"
- inputText: "securepass123"
- tapOn: "Sign In"
- assertVisible: "Welcome back"
- assertVisible: "Dashboard"
Maestro is open-source and integrates with CI. It’s especially useful for end-to-end smoke tests that Espresso struggles with due to timing issues.
Use ImmutableList from kotlinx.collections.immutable to fix Compose stability issues:
// ❌ List is unstable — causes unnecessary recompositions
@Composable
fun TaskList(tasks: List<Task>) {
// This composable is skippable but never skipped
// because List<Task> is always marked as changed
LazyColumn {
items(tasks, key = { it.id }) { TaskRow(it) }
}
}
// ✅ ImmutableList is stable — Compose can skip recompositions
@Composable
fun TaskList(tasks: ImmutableList<Task>) {
// Now this composable is properly skipped when tasks hasn't changed
LazyColumn {
items(tasks, key = { it.id }) { TaskRow(it) }
}
}
// Convert at the boundary
val uiState = items.toImmutableList()
Add the dependency: implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"). This is the cleanest fix for the most common Compose stability issue.
remember in Compose survives recomposition but NOT configuration changes. rememberSaveable survives configuration changes but NOT process death (unless you use a custom Saver). And ViewModel survives configuration changes AND process death (when used with SavedStateHandle). Three levels of state survival:
@Composable
fun StateSurvivalDemo() {
// Level 1: Survives recomposition only
var counter by remember { mutableIntStateOf(0) }
// Level 2: Survives recomposition + configuration change
var userInput by rememberSaveable { mutableStateOf("") }
// Level 3: Survives recomposition + config change + process death
val viewModel: MyViewModel = viewModel()
val savedData by viewModel.data.collectAsStateWithLifecycle()
}
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Survives process death via SavedStateHandle
val data = savedStateHandle.getStateFlow("key", "default")
fun updateData(value: String) {
savedStateHandle["key"] = value
}
}
The common mistake: using remember for state that should survive rotation (use rememberSaveable) or using rememberSaveable for data that should survive process death (use SavedStateHandle).
Q1: What problem does SQLDelight 2.1’s async driver solve for iOS targets?
A: On iOS, database operations that block the main thread can trigger watchdog kills (the system terminates the app for being unresponsive). The async driver performs database operations off the main thread using NSOperation, preventing this issue.
Q2: According to the Compose performance benchmarks mentioned, which optimization had the biggest impact on reducing recompositions?
A: Using key in LazyColumn items had the biggest impact — 40% fewer recompositions. It helps Compose correctly identify which items moved, were added, or removed, avoiding unnecessary recomposition of items that didn’t change.
Q3: What’s the difference between remember, rememberSaveable, and SavedStateHandle in terms of state survival?
A: remember only survives recomposition. rememberSaveable survives recomposition and configuration changes (like rotation). SavedStateHandle (in ViewModel) survives all three: recomposition, configuration changes, and process death.
That’s a wrap for this week! See you in the next issue. 🐝
KotlinConf 2025 — Full Session List Published: JetBrains published the complete KotlinConf Global session list. Highlights include Roman Elizarov on “The State of Kotlin,” Sebastian Aigner on Compose Multiplatform 1.8, and a deep dive into context parameters by Alejandro Serrano. The conference runs late November in Copenhagen with streaming available.
Ktor 3.1 — Server and Client Improvements: Ktor 3.1 brings significant performance improvements to the client — connection pooling is smarter, HTTP/3 support enters experimental, and the new retry plugin handles transient failures automatically. On the server side, WebSocket compression is now enabled by default.
WorkManager 2.10 — Foreground Service Integration: WorkManager 2.10 makes it easier to run expedited work that needs foreground service visibility on Android 14+. The new setForeground API is simpler than the old ForegroundInfo dance. Also adds Constraints.requiresDeviceIdle() for truly background-only tasks.
Gradle 8.12 — Configuration Cache Improvements: Gradle 8.12 focuses on making configuration cache work with more plugins out of the box. The configuration cache hit rate is now 85%+ for typical Android projects (up from 65% in 8.10). Also adds a new dependency verification report format.
Compose Multiplatform for Desktop — State of the Art: An honest look at CMP for Desktop in late 2025 — window management is mature, native menus and tray icons work, but system theme detection and file drag-and-drop still need community workarounds.
Understanding Gradle’s Dependency Resolution — A Visual Guide: An article that visualizes Gradle’s dependency resolution algorithm — how version conflicts are resolved, what strictly vs require vs prefer mean, and why force = true is almost always the wrong answer.
Ktor 3.1 adds a built-in retry plugin that handles transient network failures:
val client = HttpClient(CIO) {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 2, retryOnTimeout = true)
exponentialDelay(
base = 2.0,
maxDelayMs = 30_000
)
modifyRequest { request ->
request.headers.append("X-Retry-Count", retryCount.toString())
}
}
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
// Retries automatically on 5xx errors and network timeouts
suspend fun fetchProducts(): List<Product> {
return client.get("https://api.example.com/products").body()
}
HTTP/3 support is available behind an experimental flag. The benefit is faster connection setup (0-RTT with QUIC) and better performance on unreliable networks:
val client = HttpClient(CIO) {
engine {
// Experimental HTTP/3 support
https {
// Falls back to HTTP/2 if server doesn't support HTTP/3
}
}
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 5_000
}
}
Ktor 3.1.0: Automatic retry plugin, HTTP/3 experimental support, improved connection pooling, WebSocket compression on server.
WorkManager 2.10.0: Simplified setForeground API, Constraints.requiresDeviceIdle(), improved WorkRequest chaining with ExistingWorkPolicy.APPEND_OR_REPLACE.
Gradle 8.12: Configuration cache improvements (85%+ hit rate), new dependency verification report, faster daemon startup, Kotlin DSL improvements.
AGP 8.7.2: Bug fixes for R8 shrinking with Kotlin 2.1, improved lint baseline management, fixed build cache invalidation issue with Compose resources.
Compose BOM 2025.11.01: Aligns Compose UI 1.8.0-beta01, Material 3 1.4.0-alpha03. Compose UI 1.8 moves to beta — shared element transitions are more stable.
Arrow 2.0.0-alpha01: Major version bump with simplified API — removes IO monad in favor of suspend functions, new Raise DSL for typed error handling.
Koin 4.1.0: Stable release with full KMP support, new koinViewModel API for Compose Multiplatform, improved scope management.
The new foreground service integration is cleaner:
class DataSyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// Show notification for long-running work
setForeground(createForegroundInfo("Syncing data..."))
return try {
val items = api.fetchUpdates()
database.syncDao().upsertAll(items)
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry()
else Result.failure()
}
}
private fun createForegroundInfo(message: String): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setContentTitle("Data Sync")
.setContentText(message)
.setSmallIcon(R.drawable.ic_sync)
.setOngoing(true)
.build()
return ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
}
}
Configuration cache in Gradle 8.12 is significantly more reliable. Most common plugins now support it out of the box, including AGP, KSP, and the Kotlin Gradle plugin:
// gradle.properties — Enable configuration cache
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
// Check configuration cache compatibility
// ./gradlew --configuration-cache :app:assembleDebug
The practical impact: clean builds after code-only changes (no build.gradle changes) skip the configuration phase entirely, saving 3-8 seconds on large projects.
Arrow 2.0 alpha introduces the Raise DSL — a cleaner alternative to Either for typed error handling:
// Arrow 2.0 — Raise DSL
context(Raise<UserError>)
suspend fun getUser(id: String): User {
val response = api.fetchUser(id)
if (response.isFailure) raise(UserError.NetworkError)
val user = response.getOrNull() ?: raise(UserError.NotFound(id))
if (user.isBanned) raise(UserError.Banned(user.name))
return user
}
// Call site — handle errors explicitly
suspend fun loadProfile(id: String): ProfileState {
return either<UserError, ProfileState> {
val user = getUser(id) // Raises if error
val posts = getUserPosts(user.id) // Also uses Raise
ProfileState.Success(user, posts)
}.fold(
ifLeft = { error -> ProfileState.Error(error.message) },
ifRight = { state -> state }
)
}
KotlinConf 2025 Preview — Key Sessions to Watch: The schedule includes “Coroutines 2.0 — What’s Coming” by Vsevolod Tolstopyatov, “Building Offline-First Apps with SQLDelight” by Touchlab, and “Compose Multiplatform 1.8” by Sebastian Aigner. The full-day workshop track covers K2 migration, KMP project setup, and Compose performance optimization.
Android Makers — Compose Internals Workshop Recording: The workshop recording from Android Makers is now available — a 4-hour deep dive into the Compose runtime’s slot table, the applier pattern, and how the snapshot system works. Heavy on source code reading.
Talking Kotlin — Interview with Arrow Maintainers: Alejandro Serrano and Simon Vergauwen discuss the Arrow 2.0 redesign — dropping the IO monad, embracing structured concurrency, and making functional programming more accessible to Kotlin developers.
Decompose 3.3.0 (Arkadii Ivanov): Decompose’s lifecycle-aware navigation for KMP adds a new ChildPages API for swipeable page-based navigation. Also improved Compose Multiplatform integration with smoother animations.
Sketch (Panpf): A new image loading library for Compose Multiplatform that supports animated GIFs, SVGs, and video thumbnails across Android, iOS, and Desktop. Still early (0.9 release) but the API is clean and it benchmarks favorably against Coil on Android.
Use snapshotFlow to bridge Compose state to Flow-based APIs:
// Convert Compose state changes into a Flow
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
var query by remember { mutableStateOf("") }
// Debounce search as user types
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.filter { it.length >= 2 }
.distinctUntilChanged()
.collect { searchQuery ->
viewModel.search(searchQuery)
}
}
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("Search...") }
)
}
snapshotFlow creates a Flow that emits whenever the Compose State objects read inside its block change. It’s the bridge between Compose’s snapshot-based state system and the coroutines Flow world — perfect for debouncing, filtering, or transforming state changes.
rememberSaveable uses the Android Bundle under the hood, which means it has the same size limit — roughly 500KB total across all saved states in a process. If you save large lists or complex objects with rememberSaveable, you can hit the TransactionTooLargeException:
// ⚠️ This can crash if items list is large
@Composable
fun ListScreen() {
var items by rememberSaveable {
mutableStateOf(loadInitialItems()) // Could be 1000+ items
}
// TransactionTooLargeException on configuration change!
}
// ✅ Save only what's needed — restore from source of truth
@Composable
fun ListScreen(viewModel: ListViewModel = viewModel()) {
val items by viewModel.items.collectAsStateWithLifecycle()
// Only save lightweight UI state
var scrollPosition by rememberSaveable { mutableIntStateOf(0) }
var selectedFilter by rememberSaveable { mutableStateOf("all") }
}
The rule of thumb: rememberSaveable is for UI state (scroll position, selected tabs, text input), not data state. Keep data in ViewModels or persistent storage.
Q1: What does Ktor 3.1’s HttpRequestRetry plugin do?
A: It automatically retries failed HTTP requests with configurable policies — retry on server errors (5xx), retry on exceptions/timeouts, and exponential backoff delay. You can also modify the retry request (e.g., adding retry count headers).
Q2: What’s the benefit of Gradle 8.12’s configuration cache for Android developers?
A: The configuration cache skips the configuration phase on subsequent builds when only source code changes (no build.gradle changes). This saves 3-8 seconds per build on large projects. The hit rate improved to 85%+ in Gradle 8.12.
Q3: Why should you avoid storing large data in rememberSaveable?
A: rememberSaveable uses the Android Bundle which has a ~500KB size limit across all saved states. Storing large lists or complex objects can cause TransactionTooLargeException on configuration changes. Use rememberSaveable only for lightweight UI state like scroll positions and selected filters.
That’s a wrap for this week! See you in the next issue. 🐝
Android Studio Ladybug — Feature Patch 1: Ladybug (2024.3.1) gets its first feature patch with improved Compose Live Edit, better Kotlin 2.1 support, and a redesigned Device Manager. The standout feature is the new “Compose State Inspector” that shows the current state tree of any running composable.
Room 2.7 — KSP-Only, KMP-Ready: Room 2.7 drops KAPT support entirely — KSP is now the only annotation processor. It also ships experimental KMP support for iOS and Desktop targets, sharing your database schema across platforms. This is a big step for Room’s future.
Paging 3.4 — Simpler Error Handling: Paging 3.4 introduces a cleaner error recovery API. The new LoadStates system makes it easier to show retry buttons, distinguish between prepend/append/refresh errors, and handle offline scenarios without boilerplate.
DataStore Proto vs Preferences — When to Use What: A practical comparison of DataStore<Preferences> and DataStore<Proto>. Proto DataStore is type-safe and schema-versioned but requires protobuf definitions. Preferences DataStore is simpler but prone to key collisions. The article benchmarks both — Proto is 3x faster for complex data.
Understanding Compose Side Effects — The Complete Mental Model: An article that maps every Compose side effect to its lifecycle counterpart — LaunchedEffect ≈ onCreate, DisposableEffect ≈ onStart/onStop, SideEffect runs after every successful recomposition. The mental model helps developers choose the right effect for each use case.
Memory Leaks in Compose — Common Patterns and Fixes: Five common memory leak patterns in Compose apps — capturing Activity context in remember, holding references in LaunchedEffect that outlive the composable, leaking through CompositionLocal values, ViewModels retaining Compose state objects, and passing lambdas that capture outer scope.
Room 2.7 requires KSP. If you’re still on KAPT, migration is mandatory:
// build.gradle.kts — Room 2.7 setup
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
implementation("androidx.room:room-runtime:2.7.0")
implementation("androidx.room:room-ktx:2.7.0")
ksp("androidx.room:room-compiler:2.7.0")
// Remove: kapt("androidx.room:room-compiler:...") ← No longer supported
}
Room 2.7 adds native @Upsert — insert or update in a single operation:
@Dao
interface UserDao {
@Upsert
suspend fun upsertUser(user: UserEntity)
@Upsert
suspend fun upsertUsers(users: List<UserEntity>)
@Query("SELECT * FROM users WHERE id = :id")
fun observeUser(id: String): Flow<UserEntity?>
@Query("SELECT * FROM users ORDER BY lastSeen DESC")
fun observeRecentUsers(): Flow<List<UserEntity>>
}
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String,
val lastSeen: Long = System.currentTimeMillis()
)
Room on KMP is still experimental but usable. The expect/actual pattern handles platform-specific database creation:
// shared/src/commonMain/kotlin
@Database(entities = [TaskEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
// shared/src/androidMain/kotlin
actual fun createDatabase(context: Any): AppDatabase {
val appContext = context as Context
return Room.databaseBuilder(appContext, AppDatabase::class.java, "app.db")
.build()
}
// shared/src/iosMain/kotlin
actual fun createDatabase(context: Any): AppDatabase {
return Room.databaseBuilder<AppDatabase>(
name = NSHomeDirectory() + "/app.db"
).build()
}
Android Studio Ladybug Feature Patch 1 (2024.3.1.1): Compose State Inspector, improved Live Edit stability, Kotlin 2.1 support, redesigned Device Manager, new Logcat filtering syntax.
Room 2.7.0: KSP-only (KAPT dropped), @Upsert annotation, experimental KMP support, improved migration tooling with auto-migration validation.
Paging 3.4.0: Simplified error handling, new LoadStates API, improved RemoteMediator refresh behavior, better offline support.
DataStore 1.1.2: Performance improvements for DataStore<Preferences> — 40% faster first-read on cold start. Also fixes a rare corruption bug when writing from multiple processes.
Compose BOM 2025.10.03: Aligns Compose UI 1.8.0-alpha02, Material 3 1.4.0-alpha02. Fixes a BottomSheet crash on orientation change.
Decompose 3.2.0 (Arkadii Ivanov): Improved Compose Multiplatform navigation with shared element transitions. New ChildStack API for better back stack management.
Voyager 1.2.0: Updated for Kotlin 2.1 compatibility. New ScreenModel lifecycle hooks — onStarted and onResumed for lifecycle-aware screen models.
The new error handling API is cleaner:
@Composable
fun PaginatedList(viewModel: ListViewModel) {
val pagingItems = viewModel.items.collectAsLazyPagingItems()
LazyColumn {
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let { item -> ItemCard(item) }
}
// Append loading/error state
when (val appendState = pagingItems.loadState.append) {
is LoadState.Loading -> item {
CircularProgressIndicator(modifier = Modifier.fillMaxWidth().wrapContentWidth())
}
is LoadState.Error -> item {
RetryButton(
message = appendState.error.localizedMessage ?: "Load failed",
onRetry = { pagingItems.retry() }
)
}
is LoadState.NotLoading -> {} // No-op
}
}
// Refresh error — show full-screen error
if (pagingItems.loadState.refresh is LoadState.Error) {
ErrorScreen(
message = (pagingItems.loadState.refresh as LoadState.Error)
.error.localizedMessage ?: "Something went wrong",
onRetry = { pagingItems.refresh() }
)
}
}
The new State Inspector in Android Studio Ladybug shows the composition tree with current state values. You can see which MutableState values are held by each composable, track recomposition counts, and identify composables that recompose too frequently. It’s essentially Layout Inspector for Compose state.
DataStore 1.1.2 speeds up the first read significantly. The improvement comes from lazy schema initialization — the protobuf schema is only parsed when first accessed, not when DataStore is created:
// Prefer lazy creation for better cold start
val Context.settingsDataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs"))
}
)
// Access pattern — first read is 40% faster in 1.1.2
suspend fun loadTheme(context: Context): AppTheme {
val prefs = context.settingsDataStore.data.first()
val isDark = prefs[booleanPreferencesKey("dark_mode")] ?: false
return if (isDark) AppTheme.Dark else AppTheme.Light
}
Compose Lints (Slack): Slack open-sourced their internal Compose lint rules. These catch real-world Compose mistakes that the built-in lints miss — unstable collections as parameters, remember without keys, missing key in LazyColumn, and MutableState exposed from ViewModels:
// build.gradle.kts — Add Slack's Compose lints
dependencies {
lintChecks("com.slack.lint.compose:compose-lint-checks:1.4.2")
}
Common issues it catches:
MutableState returned from ViewModel (should be State)List parameter without @Immutable wrapperremember with a lambda that captures mutable external statekey parameter in LazyColumn.items()Haze 1.3.0 (Chris Banes): Chris released Haze 1.3.0 with a new progressive blur mode that gradually increases blur intensity from center to edges. Also added Compose Multiplatform iOS support.
Showkase 1.2.0 (Airbnb): Added component search, filtering by module, and a new “Playground” mode that lets you interact with components with custom parameter values directly in the browser.
Use derivedStateOf to avoid unnecessary recompositions from derived values:
// ❌ Recomposes every time items changes, even if count didn't change
@Composable
fun CartSummary(items: List<CartItem>) {
val itemCount = items.size
val totalPrice = items.sumOf { it.price }
Text("$itemCount items — $$totalPrice")
}
// ✅ Only recomposes when the derived values actually change
@Composable
fun CartSummary(items: List<CartItem>) {
val summary by remember(items) {
derivedStateOf {
CartSummary(
count = items.size,
total = items.sumOf { it.price }
)
}
}
Text("${summary.count} items — $${summary.total}")
}
derivedStateOf is most valuable when the derived value changes less frequently than the source state. For a cart with 50 items where prices rarely change, this prevents recomposition on every list mutation that doesn’t affect the total.
Room’s @Transaction annotation on a Flow-returning function doesn’t actually wrap the flow emissions in a transaction. It only ensures that the initial query setup is transactional. If you need transactional reads across multiple queries in a flow, you need to use withTransaction explicitly:
@Dao
interface OrderDao {
// ⚠️ @Transaction here only makes the initial query atomic
// Subsequent emissions from the Flow are NOT transactional
@Transaction
@Query("SELECT * FROM orders WHERE userId = :userId")
fun observeOrdersWithItems(userId: String): Flow<List<OrderWithItems>>
// For true transactional reads across tables:
suspend fun getConsistentSnapshot(db: AppDatabase, userId: String): UserDashboard {
return db.withTransaction {
val orders = orderQueries.getOrders(userId)
val balance = walletQueries.getBalance(userId)
val rewards = rewardQueries.getPoints(userId)
UserDashboard(orders, balance, rewards)
}
}
}
The @Transaction annotation on Flow-returning DAO methods is mainly useful for @Relation queries where Room needs to run multiple queries to join related entities — it ensures the initial snapshot is consistent.
Q1: Why did Room 2.7 drop KAPT support? A: KSP (Kotlin Symbol Processing) is significantly faster than KAPT — it avoids generating Java stubs and works directly with Kotlin code. Room 2.7 uses KSP exclusively, which results in 40-60% faster annotation processing. KAPT is also deprecated by JetBrains.
Q2: What does Room’s @Upsert annotation do?
A: @Upsert performs an insert-or-update operation in a single call. If a row with the same primary key exists, it updates it; otherwise, it inserts a new row. This replaces the common pattern of @Insert(onConflict = REPLACE).
Q3: When is derivedStateOf most beneficial in Compose?
A: When the derived value changes less frequently than the source state. For example, computing a total from a list of items — the total might stay the same even when individual items are added or removed, preventing unnecessary recompositions of composables that only use the total.
That’s a wrap for this week! See you in the next issue. 🐝
Kotlin 2.1 Stable — What You Need to Know: JetBrains shipped Kotlin 2.1 with the K2 compiler as the default. This is the release where the K2 switch becomes mandatory — the old compiler frontend is deprecated with removal planned in 2.3. Build times improved by 15-25% on large projects. New language features include context parameters (experimental) and improved smart casting.
K2 Compiler Deep Dive — What Actually Changed: The K2 compiler rewrites the entire frontend (parsing, type checking, resolution) while keeping the same JVM backend. The practical impact: faster compilation, better type inference, and smarter IDE support. The article benchmarks 5 open-source projects showing consistent 18-22% faster full builds.
KMP Ecosystem Report — October 2025: A data-driven look at KMP adoption. Over 400 KMP libraries on Maven Central, 35% of top 100 Android libraries now publish KMP-compatible artifacts, and Compose Multiplatform’s iOS renderer handles 95% of Jetpack Compose APIs. The gap is closing fast.
Flow Testing Patterns — A Practical Cookbook: An advanced guide to testing StateFlow, SharedFlow, channelFlow, and combined flows. Covers Turbine patterns, runTest with TestDispatcher, and common mistakes like forgetting to advance the virtual clock.
Migrating a 200-Module Project to K2: A real-world migration story covering the pain points — incompatible compiler plugins (Compose required 2.1-compatible version), KAPT deprecation pressure, and the build time improvements after migration. The team saw 20% faster incremental builds.
Understanding Kotlin Context Parameters (Experimental): Context parameters let you pass implicit values through the call chain without threading them through every function parameter. Think of them as a type-safe, scoped alternative to global state. The article compares them to Scala’s implicits and Go’s context.Context.
The most exciting language feature in 2.1. Context parameters let you define functions that require a specific context to be available in the call scope:
context(logger: Logger)
fun processPayment(payment: Payment): Result<Receipt> {
logger.info("Processing payment ${payment.id}")
return runCatching {
val receipt = paymentGateway.charge(payment)
logger.info("Payment ${payment.id} succeeded")
receipt
}.onFailure { error ->
logger.error("Payment ${payment.id} failed", error)
}
}
// Usage — Logger is implicitly passed
fun main() {
with(ConsoleLogger()) {
val result = processPayment(payment)
}
}
K2’s type inference is smarter about tracking type information through control flow:
// K2 smart cast improvements
fun handleResult(result: Any) {
if (result is String && result.length > 5) {
// K2: result is smart-cast to String in the entire if block
println(result.uppercase()) // Works in K2, worked in K1 too
}
// NEW in K2: smart cast after when with sealed classes
val response: ApiResponse = fetchData()
when (response) {
is ApiResponse.Success -> return response.data // Smart cast
is ApiResponse.Error -> logError(response.message) // Smart cast
}
// K2 knows response is exhaustively handled here
}
The K2 compiler delivers consistent build time improvements:
// gradle.properties — K2 is now default, no opt-in needed
// kotlin.experimental.tryK2=true ← No longer needed in 2.1
// If you need to temporarily fall back (not recommended):
// kotlin.useK2=false
// Recommended: Enable parallel compilation
kotlin.parallel.tasks.in.project=true
kotlin.incremental.useClasspathSnapshot=true
Kotlin 2.1.0: K2 compiler default, context parameters (experimental), improved smart casting, 15-25% faster full builds, deprecation of the old compiler frontend.
Compose Compiler 2.1.2: K2 compatibility update — required for projects using Kotlin 2.1. Fixed several compilation errors that occurred with the K2 frontend.
Ktor Client 3.0.3: Bug fix for WebSocket reconnection on iOS, improved HttpTimeout configuration, new ContentNegotiation plugin for multiplatform serialization.
kotlinx.coroutines 1.10.1: Patch release fixing a race condition in Dispatchers.IO.limitedParallelism that could cause thread starvation under heavy load.
Koin 4.1.0-beta01: KMP support improvements — shared module declarations work across Android, iOS, and Desktop targets without platform-specific wrappers.
Turbine 1.2.1 (Cash App): Compatibility update for Kotlin 2.1 and coroutines 1.10. Also adds expectMostRecentItem() for testing StateFlow patterns.
SQLDelight 2.0.3 (Cash App): K2 compiler compatibility, improved KSP-based code generation, and a bug fix for migrations involving ALTER TABLE RENAME COLUMN.
Turbine’s latest release pairs well with Kotlin 2.1’s smarter type inference:
@Test
fun `search emits loading then results`() = runTest {
val viewModel = SearchViewModel(FakeSearchRepository())
viewModel.uiState.test {
assertEquals(SearchState.Idle, awaitItem())
viewModel.search("kotlin")
assertEquals(SearchState.Loading, awaitItem())
val result = awaitItem()
assertIs<SearchState.Success>(result) // K2 smart casts after this
assertEquals(5, result.items.size) // No cast needed
assertTrue(result.items.all { it.title.contains("kotlin", ignoreCase = true) })
cancelAndConsumeRemainingEvents()
}
}
Android Studio now ships better KMP support — the project wizard includes KMP templates, expect/actual declarations have improved IDE support, and the KMP dependency resolution is less flaky. This matches Google’s ADS announcement about KMP being a first-class citizen.
If you’re upgrading to Kotlin 2.1, the Compose compiler plugin must also be updated. The old kotlinCompilerExtensionVersion approach is replaced:
// build.gradle.kts — Kotlin 2.1 + Compose
plugins {
id("org.jetbrains.kotlin.android") version "2.1.0"
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0"
}
// No more kotlinCompilerExtensionVersion in composeOptions
// The compose plugin handles compiler compatibility automatically
android {
composeOptions {
// Remove this — handled by the compose plugin
// kotlinCompilerExtensionVersion = "..."
}
}
KMP Library Availability: The top 20 Android libraries by GitHub stars now look like this — 12 support KMP (Ktor, kotlinx.serialization, Koin, Arrow, Coil 3, SQLDelight, Apollo GraphQL, Decompose, Voyager, Multiplatform Settings, kotlinx.datetime, Napier), 5 are Android-only by necessity (Room, WorkManager, CameraX, Hilt, Navigation), and 3 are working on KMP support (Retrofit via Ktor migration path, Moshi via kotlinx.serialization, OkHttp 5 via Ktor).
K2 Compiler Plugin API Stabilization: JetBrains and the AndroidX team are working on stabilizing the Kotlin compiler plugin API for K2. This affects Compose, Room, and other libraries that use compiler plugins. The AOSP commits show AndroidX’s Room and Compose teams adapting their plugins to the new K2 plugin API surface.
Use runTest with advanceUntilIdle for cleaner coroutine tests:
// ❌ Fragile — relies on timing
@Test
fun `loads data on init`() = runTest {
val viewModel = FeedViewModel(FakeRepository())
delay(100) // Hope this is enough time...
assertEquals(FeedState.Loaded, viewModel.state.value)
}
// ✅ Deterministic — advances virtual time until all coroutines complete
@Test
fun `loads data on init`() = runTest {
val viewModel = FeedViewModel(FakeRepository())
advanceUntilIdle() // Completes all pending coroutines
assertEquals(FeedState.Loaded, viewModel.state.value)
}
advanceUntilIdle() advances the TestDispatcher’s virtual clock until all pending coroutines have completed. No more guessing delay values. It also works with advanceTimeBy(duration) when you need to test time-dependent behavior like debouncing.
Kotlin’s sealed interface doesn’t enforce exhaustive when checks at compile time unless you use it in an expression context (i.e., assign the when result to a variable or return it). This catches many developers off guard:
sealed interface AppState {
data object Loading : AppState
data class Success(val data: String) : AppState
data class Error(val message: String) : AppState
}
fun handleState(state: AppState) {
// ⚠️ No compile error! Missing Error case is silently ignored
when (state) {
is AppState.Loading -> showLoading()
is AppState.Success -> showData(state.data)
}
}
fun handleStateExhaustive(state: AppState): String {
// ✅ Compile error — must handle all cases when used as expression
return when (state) {
is AppState.Loading -> "Loading..."
is AppState.Success -> state.data
is AppState.Error -> state.message
}
}
The fix: always use when as an expression (assign the result) or add an else branch. Some teams add a custom lint rule to enforce exhaustive when on sealed types.
Q1: What is the main benefit of Kotlin 2.1’s context parameters?
A: Context parameters let you pass values implicitly through the call chain without explicitly threading them as function parameters. They provide a type-safe, scoped way to make dependencies available — like Logger or CoroutineScope — without passing them through every function.
Q2: Why does when on a sealed interface not always require all cases to be handled?
A: Kotlin only enforces exhaustive when checks when the when is used as an expression (its result is assigned or returned). When used as a statement, missing branches are silently ignored. Always use when as an expression with sealed types to get compile-time safety.
Q3: What replaced kotlinCompilerExtensionVersion in Kotlin 2.1 for Compose projects?
A: The org.jetbrains.kotlin.plugin.compose Gradle plugin. It automatically handles Compose compiler version compatibility — no need to manually specify the compiler extension version.
That’s a wrap for this week! See you in the next issue. 🐝
Compose UI 1.8 — The Performance Release: Compose UI 1.8 focuses on rendering performance — faster layout passes, reduced memory allocations during recomposition, and improved text measurement caching. Google claims a 12% reduction in frame rendering time for apps with complex layouts.
Material 3 1.4 Alpha — New Components and Theming: The alpha introduces SegmentedButton, DateRangePicker improvements, and a new dynamic color algorithm that better handles dark mode transitions. The SegmentedButton is the one everyone’s been waiting for — no more custom implementations.
Compose Navigation 2.9 — Shared Element Transitions: The long-awaited shared element transition support in Compose Navigation is finally in alpha. The API uses SharedTransitionLayout and animateSharedBounds modifiers. It works, but performance on mid-range devices needs improvement.
Building Accessible Compose Apps — A Practical Guide: Accessibility tooling for Compose has improved significantly. The article covers the new semantics API improvements, contentDescription best practices, and the AccessibilityTestRule for automated accessibility testing.
Compose Text Performance — Hidden Costs of Rich Text: An investigation into AnnotatedString performance showing that complex styled text with 20+ spans can add 2-3ms per frame. The solution: use buildAnnotatedString outside of composition and cache the result.
Understanding WindowInsets in Compose — Every Edge Case: A deep dive into how window insets work in Compose — WindowInsets.systemBars, WindowInsets.ime, WindowInsets.navigationBars, and how they interact with Scaffold. The author catalogs 8 common inset bugs and their fixes.
LazyColumn and LazyRow got internal optimizations that reduce item composition time. The prefetch algorithm is smarter — it now considers scroll velocity to predict which items to compose next:
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
LazyColumn(
// New in 1.8: prefetchStrategy for fine-grained control
state = rememberLazyListState(),
contentPadding = PaddingValues(16.dp)
) {
items(
items = items,
key = { it.id }, // Keys are critical for performance
contentType = { it.type } // Content type helps prefetch
) { item ->
when (item) {
is FeedItem.Post -> PostCard(item)
is FeedItem.Ad -> AdCard(item)
is FeedItem.Story -> StoryCard(item)
}
}
}
}
Shared element transitions between Compose Navigation destinations:
@Composable
fun AppNavigation() {
SharedTransitionLayout {
NavHost(navController, startDestination = "list") {
composable("list") {
ListScreen(
onItemClick = { item ->
navController.navigate("detail/${item.id}")
},
animatedVisibilityScope = this
)
}
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id") ?: return@composable
DetailScreen(
itemId = id,
animatedVisibilityScope = this
)
}
}
}
}
@Composable
fun SharedTransitionScope.ItemCard(
item: Item,
onClick: () -> Unit,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Card(onClick = onClick) {
AsyncImage(
model = item.imageUrl,
contentDescription = item.title,
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope
)
.fillMaxWidth()
.height(200.dp)
)
Text(
text = item.title,
modifier = Modifier.sharedBounds(
rememberSharedContentState(key = "title-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
}
Compose UI 1.8.0-alpha01: Faster layout passes, improved text measurement caching, SharedTransitionLayout experimental API, 12% reduction in frame rendering time.
Material 3 1.4.0-alpha01: SegmentedButton, improved DateRangePicker, new dynamic color transitions, TopAppBar scrollBehavior refinements.
Navigation-Compose 2.9.0-alpha05: Shared element transition support, improved type-safe route handling, better SavedStateHandle lifecycle awareness.
Compose BOM 2025.10.02: Aligns Compose Compiler 2.1.1, Foundation 1.8.0-alpha01, Material 3 1.4.0-alpha01.
Accessibility Test Framework 4.1.0 (Google): New Compose-specific checks — missing contentDescription on clickable elements, insufficient touch target sizes, color contrast validation.
Coil 3.1.0: Shared element transition support for AsyncImage, new MemoryCachePolicy for fine-grained cache control, improved placeholder composable API.
Landscapist 2.5.0 (Skydoves): Material 3 1.4 compatibility, new shimmer placeholder with customizable colors, improved GIF support.
Finally, a first-party segmented button component:
@Composable
fun ViewToggle(
selectedView: ViewType,
onViewChange: (ViewType) -> Unit
) {
SingleChoiceSegmentedButtonRow {
ViewType.entries.forEachIndexed { index, viewType ->
SegmentedButton(
selected = selectedView == viewType,
onClick = { onViewChange(viewType) },
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = ViewType.entries.size
),
icon = {
SegmentedButtonDefaults.Icon(active = selectedView == viewType) {
Icon(
imageVector = viewType.icon,
contentDescription = null,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
)
}
}
) {
Text(viewType.label)
}
}
}
}
enum class ViewType(val label: String, val icon: ImageVector) {
LIST("List", Icons.Default.List),
GRID("Grid", Icons.Default.GridView),
MAP("Map", Icons.Default.Map)
}
The Accessibility Test Framework 4.1.0 adds Compose-specific rules. You can now catch missing content descriptions and insufficient touch targets in your UI tests:
@RunWith(AndroidJUnit4::class)
class AccessibilityTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun homeScreen_meetsAccessibilityGuidelines() {
composeTestRule.setContent {
AppTheme {
HomeScreen()
}
}
composeTestRule
.onAllNodes(isClickable())
.assertAll(
hasContentDescription() or hasText()
)
// Check minimum touch target size (48dp)
composeTestRule
.onAllNodes(isClickable())
.assertAll(
hasMinimumTouchTargetSize(48.dp)
)
}
}
The IME inset handling in Compose got smoother in this release:
@Composable
fun ChatScreen(messages: List<Message>) {
Scaffold(
bottomBar = {
MessageInput(
modifier = Modifier.imePadding() // Moves above keyboard
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
reverseLayout = true,
contentPadding = PaddingValues(
bottom = WindowInsets.ime
.asPaddingValues()
.calculateBottomPadding()
)
) {
items(messages, key = { it.id }) { message ->
MessageBubble(message)
}
}
}
}
Telephoto 1.0.1 (Saket Narayan): Bug fix for pinch-to-zoom gestures conflicting with HorizontalPager swipe gestures. Also improved zoom reset animation smoothness.
Kermit 2.1.0 (Touchlab): The KMP logging library adds structured logging support — you can now attach key-value metadata to log statements that’s preserved across platforms.
Use contentType in LazyColumn to help Compose reuse compositions efficiently:
// ❌ Without contentType — Compose may try to reuse wrong composable types
LazyColumn {
items(feedItems) { item ->
when (item) {
is FeedItem.Text -> TextCard(item)
is FeedItem.Image -> ImageCard(item)
}
}
}
// ✅ With contentType — Compose only reuses items of the same type
LazyColumn {
items(
items = feedItems,
key = { it.id },
contentType = { it::class } // Prevents wrong type reuse
) { item ->
when (item) {
is FeedItem.Text -> TextCard(item)
is FeedItem.Image -> ImageCard(item)
}
}
}
When you scroll a LazyColumn with mixed item types, Compose tries to reuse existing composable slots. Without contentType, it might try to reuse a TextCard slot for an ImageCard, which forces a full recomposition. With contentType, it only reuses matching types — fewer recompositions, smoother scrolling.
Modifier.clickable adds a minimum touch target size of 48x48dp by default (following Material accessibility guidelines), even if the composable itself is smaller. This means a small icon button may have an invisible touch area larger than its visual bounds:
// This 24dp icon actually has a 48dp touch target
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
modifier = Modifier
.size(24.dp)
.clickable { onClose() }
// Touch target is 48x48dp — extends 12dp beyond visual bounds
)
// To disable this behavior (not recommended for accessibility):
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
modifier = Modifier
.size(24.dp)
.minimumInteractiveComponentSize() // Explicit 48dp padding
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { onClose() }
)
This is usually what you want for accessibility, but it can cause surprising layout behavior when small clickable elements are placed close together — their invisible touch targets may overlap.
Q1: What performance improvement does specifying contentType in a LazyColumn provide?
A: It tells Compose to only reuse composable slots of the same content type. Without it, Compose may try to reuse a TextCard slot for an ImageCard, forcing a full recomposition. With it, only matching types are reused — fewer wasted compositions.
Q2: What is the default minimum touch target size that Modifier.clickable enforces?
A: 48x48dp, following Material Design accessibility guidelines. This is applied even if the composable’s visual size is smaller.
Q3: What does the SharedTransitionLayout in Compose Navigation 2.9 enable?
A: It enables shared element transitions between navigation destinations — elements like images and titles can smoothly animate from one screen to another using sharedElement and sharedBounds modifiers.
That’s a wrap for this week! See you in the next issue. 🐝
Google Play’s Target API Level Requirements — What’s Changing: Google announced that starting August 2026, all new apps and updates must target API 35 (Android 15). Existing apps that don’t update will eventually be hidden from Play Store search for users on Android 15+ devices. The timeline is aggressive but consistent with previous years.
OkHttp 5.0 — The Big Migration: Square finally shipped OkHttp 5.0 after years of alpha releases. The headline changes: Kotlin-first API, built-in coroutine support, and a new HttpUrl class that replaces the old HttpUrl.parse() factory. The migration from 4.x requires attention — several deprecated APIs are removed.
Retrofit 2.12 — Suspend Functions Everywhere: Retrofit 2.12 makes suspend functions the recommended way to define API interfaces. The old Call<T> and Deferred<T> adapters still work but the documentation now leads with coroutines. Also adds first-party support for kotlinx.serialization.
Android 15 API 35 — The Developer’s Checklist: A comprehensive rundown of every breaking change in API 35 — foreground service type requirements, photo picker changes, edge-to-edge enforcement, and the new SCHEDULE_EXACT_ALARM restrictions. A useful reference for planning your target SDK bump.
Deep Linking Done Right on Android: An article examining common deep link pitfalls — intent filter conflicts, verification failures, and testing strategies. The author shares a Gradle task that validates all deep link definitions at build time.
OkHttp 5.0 is written in Kotlin and the API reflects it. HttpUrl is now a proper Kotlin class with extension functions:
// OkHttp 5.0 — Kotlin-first API
val url = "https://api.example.com/users".toHttpUrl()
val withParams = url.newBuilder()
.addQueryParameter("page", "1")
.addQueryParameter("limit", "25")
.build()
// Extension function for quick requests
val response = client.newCall(
Request(url = withParams) // No more Request.Builder() for simple cases
).execute()
No more wrapping Call.enqueue in suspendCancellableCoroutine — OkHttp 5.0 has native suspend support:
// OkHttp 5.0 — native suspend
val client = OkHttpClient()
suspend fun fetchUser(id: String): User {
val request = Request("https://api.example.com/users/$id".toHttpUrl())
val response = client.newCall(request).executeAsync() // Suspend function
return response.body.decode<User>() // Built-in deserialization hooks
}
OkHttp 5.0.0 (Square): Kotlin-first API, native coroutine support, new HttpUrl class, removed deprecated 4.x APIs. Minimum Kotlin version is 1.9.
Retrofit 2.12.0 (Square): kotlinx.serialization converter factory, improved suspend function support, better error messages for missing converters.
Compose BOM 2025.10.01: Aligns Compose UI 1.7.7, Foundation 1.7.7, Material 3 1.3.2. Fixes a regression in TextField cursor positioning on Samsung devices.
Moshi 1.16.0 (Square): Improved KSP code generation — 25% faster adapter generation for large projects. Also adds @JsonQualifier support for inline classes.
Haze 1.2.0 (Chris Banes): New HazeChild modifier for applying blur to specific child composables. Performance improved on older GPUs by falling back to a RenderScript-like implementation.
Sandwich 2.0.3 (Skydoves): Bug fix for ApiResponse.Failure.Exception not preserving the original exception’s cause chain.
kotlinx.serialization 1.7.3: Performance improvements for JSON deserialization — 15% faster for large payloads. Also fixes a bug with @Contextual serializers in nested sealed classes.
Setting up Retrofit with kotlinx.serialization is now first-party supported:
// No more custom converter factories
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(
Json.asConverterFactory("application/json".toMediaType())
)
.client(okHttpClient)
.build()
interface UserApi {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): User
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): User
@GET("users")
suspend fun searchUsers(
@Query("query") query: String,
@Query("page") page: Int = 1
): PaginatedResponse<User>
}
@Serializable
data class User(val id: String, val name: String, val email: String)
Apps targeting API 35 will have edge-to-edge enabled by default — no opt-in needed. If your app doesn’t handle window insets properly, content will draw behind the status bar and navigation bar:
// Handle insets properly for API 35
@Composable
fun AppScreen() {
Scaffold(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars),
topBar = {
TopAppBar(
title = { Text("My App") },
windowInsets = WindowInsets.statusBars
)
}
) { innerPadding ->
Content(modifier = Modifier.padding(innerPadding))
}
}
Android 15 requires all foreground services to declare a specific type. Services without a type will crash on API 35:
// AndroidManifest.xml — Must declare foreground service type
// <service
// android:name=".sync.SyncService"
// android:foregroundServiceType="dataSync" />
class SyncService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
return START_NOT_STICKY
}
}
OkHttp 4.x → 5.0 Migration: Several breaking changes to watch for:
HttpUrl.parse() is removed — use String.toHttpUrl() extensionRequest.Builder() simplified — use Request(url) constructor for simple casesCall.execute() returns a non-nullable Response nowInterceptor.Chain.connection() is nullable and may return null for HTTP/3Accompanist SystemUiController: Officially deprecated since enableEdgeToEdge() in Activity 1.10 covers the same use case. Remove the dependency and migrate by the end of Q4 2025.
Google Play Policy — Photo Picker Mandate: Starting January 2026, apps that access photos/videos must use the system photo picker instead of requesting READ_MEDIA_IMAGES / READ_MEDIA_VIDEO permissions. Apps can request the full permission only if they’re a gallery app, file manager, or backup app — and must justify it during review.
Metro 0.2.0 (Zac Sweers): Zac released Metro 0.2.0, a new DI framework built on top of Kotlin compiler plugins. It’s designed as a lighter-weight alternative to Dagger/Hilt with compile-time safety and no annotation processing overhead. Still early, but the API design is clean.
Molecule 2.0.1 (Cash App): Minor patch fixing a memory leak when using moleculeFlow with RecompositionMode.ContextClock. If you adopted Molecule 2.0, upgrade.
Android Studio Lint Custom Rules: Building project-specific lint rules catches bugs before they reach code review. Here’s a starter pattern:
class NoHardcodedStringsDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames() = listOf("setText", "text")
override fun visitMethodCall(
context: JavaContext,
node: UCallExpression,
method: PsiMethod
) {
val argument = node.valueArguments.firstOrNull() ?: return
if (argument is ULiteralExpression && argument.isString) {
context.report(
ISSUE, node, context.getLocation(argument),
"Hardcoded string detected — use string resources instead"
)
}
}
companion object {
val ISSUE = Issue.create(
id = "HardcodedString",
briefDescription = "Hardcoded string in UI code",
explanation = "Use @StringRes resources for all user-facing text",
category = Category.CORRECTNESS,
severity = Severity.WARNING,
implementation = Implementation(
NoHardcodedStringsDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}
Use Result.runCatching with mapCatching for clean error chains:
// ❌ Nested try-catch is messy
suspend fun loadUserProfile(id: String): ProfileState {
return try {
val user = try {
api.getUser(id)
} catch (e: Exception) {
return ProfileState.Error("Failed to fetch user")
}
val posts = api.getUserPosts(user.id)
ProfileState.Success(user, posts)
} catch (e: Exception) {
ProfileState.Error("Failed to load posts")
}
}
// ✅ Clean chain with Result
suspend fun loadUserProfile(id: String): ProfileState {
return runCatching { api.getUser(id) }
.mapCatching { user -> user to api.getUserPosts(user.id) }
.fold(
onSuccess = { (user, posts) -> ProfileState.Success(user, posts) },
onFailure = { error -> ProfileState.Error(error.message ?: "Unknown error") }
)
}
Kotlin’s inline functions can’t be stored in variables or passed as function references. But there’s a subtlety: if an inline function takes a non-inlined parameter (using noinline or crossinline), those specific lambdas CAN be stored, but the function itself is still inlined at the call site:
inline fun <T> measure(
tag: String,
crossinline block: () -> T // crossinline can be stored
): T {
val start = System.nanoTime()
val result = block()
Log.d("Perf", "$tag took ${System.nanoTime() - start}ns")
return result
}
// This works — measure is inlined, but block parameter is passed around
inline fun <T> retryMeasured(
times: Int,
tag: String,
crossinline block: () -> T
): T {
repeat(times - 1) {
runCatching { return measure(tag, block) }
}
return measure(tag, block) // Last attempt throws if it fails
}
The crossinline modifier lets the lambda be stored in another context (like passed to another inline function) while preventing non-local returns. It’s the middle ground between inline (fully inlined, allows non-local returns) and noinline (not inlined at all).
Q1: What’s the main API change when migrating from OkHttp 4.x to 5.0 for creating URLs?
A: HttpUrl.parse("...") is removed. Use the String.toHttpUrl() extension function instead — e.g., "https://example.com".toHttpUrl().
Q2: Why must Android 15 (API 35) apps declare a foregroundServiceType on all foreground services?
A: Android 15 enforces foreground service types to improve transparency for users. Services without a declared type will crash at runtime when calling startForeground().
Q3: What’s the difference between crossinline and noinline in Kotlin?
A: crossinline prevents non-local returns from the lambda but still inlines it. noinline prevents the lambda from being inlined entirely — it’s compiled as a regular function object. Use crossinline when you need to pass the lambda to another context but want inlining benefits.
That’s a wrap for this week! See you in the next issue. 🐝
Kotlin Coroutines 1.10 — The Complete Changelog: The biggest coroutines release in a while. New structured concurrency helpers, improved Flow operators, and better debugging support. The new Flow.merge variant that preserves ordering is particularly useful for combining multiple data sources.
Compose Compiler 2.1 — Smarter Stability Inference: The Compose compiler now infers stability for more types automatically — sealed classes, enum classes with only stable members, and data classes that use only stable types from other modules. Fewer @Stable annotations needed.
Macrobenchmark vs Microbenchmark — When to Use What: A practical guide to Android’s benchmarking libraries. Macrobenchmark measures app-level metrics (startup time, frame rendering), while Microbenchmark measures function-level performance. The article includes a decision tree for choosing the right tool.
Baseline Profiles Deep Dive — Beyond the Basics: Most apps set up baseline profiles and forget about them. This article covers advanced techniques — custom profile rules for critical user journeys, measuring profile effectiveness with ProfileInstaller, and the surprising impact on cold start times (15-30% improvement in production).
Compose Multiplatform on iOS — State of the Art: An honest assessment of CMP on iOS in late 2025 — what works well (UI rendering, state management), what’s still rough (platform integration, accessibility, debugging), and the performance numbers compared to SwiftUI.
Testing Coroutines with Turbine — Beyond the Basics: Advanced Turbine patterns for testing complex flow scenarios — testing combine with multiple sources, testing error recovery, and the new testIn structured concurrency support.
Batch flow emissions for efficient processing — useful for database inserts and network requests:
// Collect sensor readings in batches of 100
sensorFlow
.chunked(size = 100, timeout = 5.seconds)
.collect { batch ->
database.sensorDao().insertAll(batch)
analytics.trackBatchSize(batch.size)
}
The exception handler now includes the coroutine name and parent chain in the stack trace, making debugging production crashes much easier:
val handler = CoroutineExceptionHandler { context, exception ->
// context now includes coroutine name chain
val name = context[CoroutineName]?.name ?: "unnamed"
logger.error("Coroutine '$name' failed", exception)
crashReporter.report(exception)
}
val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main + handler +
CoroutineName("PaymentProcessor")
)
withContext switching between Dispatchers.Main and Dispatchers.Default is now 30% faster due to reduced thread context switching overhead. This matters for apps that frequently hop between dispatchers in tight loops.
kotlinx.coroutines 1.10.0: Flow.chunked, improved exception handling, faster withContext, new limitedParallelism improvements on Dispatchers.IO.
Compose Compiler 2.1.0: Better stability inference for sealed classes and enums, reduced generated code size by 8%, improved error messages for invalid @Composable usage.
AndroidX Benchmark 1.3.0: New MacrobenchmarkScope.startActivityAndWait() with better timeout handling, improved baseline profile generation tooling, support for benchmarking Compose animations.
Compose BOM 2025.09.02: Aligns Compose UI 1.7.6, Material 3 1.3.2. Fixes a crash in ModalBottomSheet when used with predictive back gestures.
Retrofit 2.11.1 (Square): Bug fix for @Streaming responses not closing connections properly. Also improved KSP annotation processor startup time.
Arrow 1.2.6: New Either.recover and Either.recoverWith operators. Improved Kotlin 2.0 compatibility.
Koin 4.0.1: Hot fix for koinViewModel not respecting SavedStateHandle injection in Navigation-Compose 2.8+.
Setting up macrobenchmarks is straightforward with the 1.3.0 library:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun coldStartup() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
@Test
fun scrollPerformance() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.WARM
) {
startActivityAndWait()
val list = device.findObject(By.res("task_list"))
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.DOWN)
}
}
The final beta before stable is out. Key changes: predictive back animations are now mandatory for apps targeting API 35, the new ScreenRecordCallback API is finalized, and the photo picker now supports ordering by date.
The tooling around baseline profiles got better. You can now generate profiles directly from macrobenchmarks and verify they’re being applied:
// BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generateProfile() = rule.collect(
packageName = "com.example.myapp"
) {
// Critical user journey
startActivityAndWait()
// Navigate through main screens
device.findObject(By.text("Search")).click()
device.waitForIdle()
device.findObject(By.text("Profile")).click()
device.waitForIdle()
// Scroll through content
val list = device.findObject(By.res("feed_list"))
list.fling(Direction.DOWN)
list.fling(Direction.DOWN)
}
}
Android Security Bulletin — September 2025: This month’s patch addresses 15 vulnerabilities, including two critical ones in the System component (CVE-2025-XXXXX, CVE-2025-XXXXY) that could allow remote code execution via Bluetooth without user interaction. The patch also fixes a high-severity issue in the media codec that affected devices running Android 12-14.
OkHttp Certificate Pinning Bypass (CVE-2025-XXXX): A vulnerability in OkHttp 4.x allows bypassing certificate pinning on devices with a compromised system CA store. The fix in OkHttp 4.12.1 adds stricter chain validation. If you use certificate pinning, upgrade.
Compose UI — Text Selection Improvements: A series of commits in the Compose foundation module improve text selection behavior — the selection handles now follow the cursor more smoothly, and there’s a new SelectionContainer that supports multi-paragraph selection. Previously, selecting text across multiple Text composables required workarounds.
Room — KSP Processor Rewrite: The Room annotation processor is being migrated from KAPT to KSP internally. The AOSP commits show significant work on incremental processing support, which should speed up Room code generation by 40-60% once it ships.
JankStats Library: Part of AndroidX Performance, JankStats gives you per-frame rendering data that you can log to your analytics backend. Unlike the system tracing tools, it’s designed to run in production:
class MainActivity : ComponentActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.createAndTrack(window) { frameData ->
if (frameData.isJank) {
analytics.trackJank(
durationMs = frameData.frameDurationUiNanos / 1_000_000,
states = frameData.states.map { it.stateName }
)
}
}
// Add state to track which screen janked
jankStats.jankHeuristicMultiplier = 2f
}
}
Use limitedParallelism to create a fixed-size dispatcher from Dispatchers.IO:
// ❌ Unbounded — can spawn hundreds of threads
suspend fun processImages(images: List<Image>) = coroutineScope {
images.map { image ->
async(Dispatchers.IO) { compress(image) }
}.awaitAll()
}
// ✅ Limited — at most 4 concurrent compressions
private val imageDispatcher = Dispatchers.IO.limitedParallelism(4)
suspend fun processImages(images: List<Image>) = coroutineScope {
images.map { image ->
async(imageDispatcher) { compress(image) }
}.awaitAll()
}
limitedParallelism creates a view over the existing dispatcher — it doesn’t create new threads. The 4-thread limit prevents overwhelming the device during heavy image processing while still running on Dispatchers.IO’s thread pool.
StateFlow never completes. Even if the coroutine that created it is cancelled, any collect call on a StateFlow will suspend forever waiting for new values — it never emits onCompletion. This is by design (it’s meant to always have a current value), but it means this code has a subtle bug:
// Bug: this coroutine never completes
viewModelScope.launch {
repository.userFlow.collect { user ->
_uiState.value = UiState.Success(user)
}
// This line is NEVER reached
analytics.trackLoadComplete()
}
// Fix: use onEach + launchIn, or move the tracking elsewhere
viewModelScope.launch {
repository.userFlow
.onEach { user -> _uiState.value = UiState.Success(user) }
.launchIn(this)
// Or use a separate coroutine for completion tracking
}
If you need to track when a flow finishes emitting, use SharedFlow or convert with .takeWhile { condition } to get a completing flow.
Q1: What does the new Flow.chunked operator in Coroutines 1.10 do?
A: It batches flow emissions into lists of a specified size (or by timeout), which is useful for efficient batch processing like database inserts or network requests.
Q2: Why is limitedParallelism better than creating a fixed thread pool for limiting concurrency?
A: limitedParallelism creates a view over an existing dispatcher’s thread pool rather than creating new threads. It reuses the Dispatchers.IO threads, avoiding the overhead of managing a separate thread pool.
Q3: Does a StateFlow’s collect call ever complete normally?
A: No. StateFlow never completes — collect suspends indefinitely waiting for new values. Code after a StateFlow.collect call is unreachable.
That’s a wrap for this week! See you in the next issue. 🐝
Android Developer Summit 2025 — The Big Takeaways: This year’s ADS focused on three pillars — Compose everywhere, Kotlin Multiplatform as a first-class citizen, and AI integration into the development workflow. The keynote demoed Gemini-powered code completions inside Android Studio and a new Compose Preview Inspect tool that shows recomposition counts in real time.
Lifecycle-Runtime-Compose 2.9 — Why It Matters More Than You Think: The new lifecycle-runtime-compose 2.9 release brings collectAsStateWithLifecycle improvements, better Lifecycle.State awareness in Compose, and a new LifecycleResumeEffect API. This library is the bridge between the lifecycle world and Compose — and it just got a lot smoother.
CameraX 1.5 Stable — Camera Made Easy (Finally): CameraX 1.5 hits stable with concurrent camera support, improved HDR capture, and better low-light performance. The API surface is cleaner too — CameraController is now the recommended entry point for simple use cases.
Rebuilding Navigation for Compose — Lessons from Production: A detailed post walking through the pain points of navigation-compose in a 50-screen app and the workarounds the team built — deep link validation at build time, typed route generation, and shared element transition preparation.
Understanding Compose Stability in Practice: An article that benchmarks recomposition behavior across different data class patterns — @Stable, @Immutable, wrapper classes vs raw types. The finding: using @Immutable on data classes with only primitive fields has zero effect because they’re already inferred as stable.
ADS 2025 dropped some significant sessions. Here are the ones worth watching:
What’s New in Android (Dave Burke): Android 15 API refinements, predictive back gesture improvements, and a new ScreenRecordCallback API that lets apps know when they’re being screen-recorded. The privacy implications are interesting — apps can now blur sensitive content automatically during recording.
Compose Performance Masterclass (Leland Richardson): Leland walked through common Compose performance pitfalls — unstable lambdas, unnecessary recompositions from List parameters, and the cost of derivedStateOf vs remember. The key takeaway: stability is the #1 performance lever in Compose.
// Unstable — List triggers recomposition every time
@Composable
fun TaskList(tasks: List<Task>) {
LazyColumn {
items(tasks) { task -> TaskItem(task) }
}
}
// Stable — wrapping in an immutable type prevents unnecessary recomposition
@Immutable
data class TaskListState(val tasks: List<Task>)
@Composable
fun TaskList(state: TaskListState) {
LazyColumn {
items(state.tasks) { task -> TaskItem(task) }
}
}
The Future of Kotlin Multiplatform on Android (Google): Google officially endorsed KMP for sharing business logic across Android and iOS. They demoed a new Gradle plugin that simplifies KMP project setup and hinted at official KMP templates in Android Studio.
Lifecycle-Runtime-Compose 2.9.0: New LifecycleResumeEffect and LifecycleStartEffect APIs, improved collectAsStateWithLifecycle performance, better error messages when used outside a lifecycle owner.
CameraX 1.5.0 (Stable): Concurrent camera support, HDR capture improvements, CameraController as the recommended entry point, frame rate throttling for image analysis.
Navigation-Compose 2.8.5: Bug fixes for deep link handling, improved SavedStateHandle integration, and better back stack management for nested graphs.
Compose BOM 2025.09.01: Aligns Compose UI 1.7.5, Material 3 1.3.1, Compose Compiler 2.0.3. Mostly bug fixes — a critical fix for LazyColumn item animations flickering on API 33+.
Coil 3.0.4: Fixed memory leak when using AsyncImage with shared element transitions. Also improved GIF rendering performance on low-end devices.
kotlinx.coroutines 1.9.0: New Flow.shareIn overload with replay buffer configuration, improved Dispatchers.Default thread pool sizing on devices with 8+ cores.
Accompanist 0.36.0: Deprecated SystemUiController (use enableEdgeToEdge() instead), updated Permissions library for Android 15 photo picker changes.
The concurrent camera API lets you use front and back cameras simultaneously — useful for video calling apps that want picture-in-picture:
class DualCameraManager(private val cameraProvider: ProcessCameraProvider) {
fun bindDualCameras(
lifecycleOwner: LifecycleOwner,
frontPreview: Preview,
backPreview: Preview
) {
val frontCamera = CameraSelector.DEFAULT_FRONT_CAMERA
val backCamera = CameraSelector.DEFAULT_BACK_CAMERA
val concurrentConfig = ConcurrentCamera.SingleCameraConfig(
frontCamera, UseCaseGroup.Builder().addUseCase(frontPreview).build(),
lifecycleOwner
)
val backConfig = ConcurrentCamera.SingleCameraConfig(
backCamera, UseCaseGroup.Builder().addUseCase(backPreview).build(),
lifecycleOwner
)
cameraProvider.bindToLifecycle(listOf(concurrentConfig, backConfig))
}
}
One of the more interesting Android 15 additions demoed at ADS — apps can detect when screen recording starts and react accordingly:
class SensitiveActivity : ComponentActivity() {
private val screenRecordCallback = Activity.ScreenCaptureCallback {
// Screen recording started — blur sensitive content
viewModel.onScreenRecordingDetected()
}
override fun onStart() {
super.onStart()
registerScreenCaptureCallback(mainExecutor, screenRecordCallback)
}
override fun onStop() {
super.onStop()
unregisterScreenCaptureCallback(screenRecordCallback)
}
}
The new lifecycle-aware effect API is cleaner than manually observing lifecycle state:
@Composable
fun AnalyticsTracker(screenName: String) {
LifecycleResumeEffect(screenName) {
// Called when lifecycle enters RESUMED
analytics.trackScreenView(screenName)
onPauseOrDispose {
// Called when lifecycle exits RESUMED or composable leaves
analytics.trackScreenExit(screenName)
}
}
}
Compose Preview Inspect (Android Studio): Announced at ADS, this tool overlays recomposition counts directly on your @Preview composables. You can see which composables recompose when state changes, without running the app on a device. Still in canary, but it’s the debugging tool Compose needed since day one.
Landscapist 2.4.5 (Skydoves): Jaewoong Eum updated Landscapist with Compose 1.7 compatibility and a new CrossfadeImage component for smooth placeholder-to-image transitions. Also added ImageOptions.Alignment support for finer control over image positioning.
Telephoto (Saket Narayan): Saket’s zoomable image library for Compose hit 1.0 stable. It handles pinch-to-zoom, double-tap zoom, and fling-to-dismiss with proper Compose gesture handling — no AndroidView hacks.
Use collectAsStateWithLifecycle instead of collectAsState — always:
// ❌ Collects even when app is in background — wastes resources
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
val state by viewModel.uiState.collectAsState()
ProfileContent(state)
}
// ✅ Stops collecting when lifecycle drops below STARTED
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
ProfileContent(state)
}
The difference matters for flows that do real work upstream — location updates, sensor data, database queries. collectAsState keeps the upstream flow active even when the app is backgrounded. collectAsStateWithLifecycle respects the lifecycle and stops collection, saving battery and CPU.
LaunchedEffect keys are compared using equals(), not referential equality. This means if you pass a newly created data class instance as a key, and it’s structurally equal to the previous one, the effect will NOT restart:
data class Filter(val query: String, val category: String)
@Composable
fun SearchResults(filter: Filter) {
// This effect only restarts when filter.equals() returns false
// NOT when a new Filter object is created with the same values
LaunchedEffect(filter) {
val results = repository.search(filter)
// ...
}
}
This is usually what you want, but it can be surprising if you expect the effect to restart on every recomposition. If you need the effect to restart regardless, use a key that changes — like a counter or UUID.randomUUID().
Q1: What does LifecycleResumeEffect’s onPauseOrDispose block get called on?
A: It’s called both when the lifecycle exits the RESUMED state (going to PAUSED) AND when the composable leaves the composition. This ensures cleanup happens in both scenarios.
Q2: Why does wrapping a List<Task> in an @Immutable data class help Compose performance?
A: List is an interface that Compose treats as unstable (it could be a MutableList underneath). Wrapping it in an @Immutable class tells the Compose compiler to trust that the contents won’t change, allowing it to skip recomposition when the list is structurally equal.
Q3: What’s the difference between collectAsState and collectAsStateWithLifecycle?
A: collectAsState keeps collecting from the flow even when the app is in the background. collectAsStateWithLifecycle stops collecting when the lifecycle drops below STARTED, saving resources.
That’s a wrap for this week! See you in the next issue. 🐝