📚 Articles & References
Google — “What’s New in Jetpack Compose April ‘26 Release”: The Compose April ‘26 release is now stable. Highlights: BasicHtmlText graduated to stable, LazyColumn velocity-aware prefetch is stable, improved SelectionContainer for multi-touch, the new Modifier.onFocusedBoundsChanged API, and a complete overhaul of the TextField focus management that eliminates a class of focus-stealing bugs in multi-field forms.
Google — “Android Studio Panda 4: Planning Mode and Next Edit Prediction”: Android Studio Panda 4 is now stable. The big additions: Planning Mode (AI describes what it will do before making changes — similar to Claude’s extended thinking), Next Edit Prediction (predicts the next code change you’ll make based on context), and LeakCanary integration directly in the Profiler as a dedicated task. The Gemini model powering Studio’s AI features has been updated.
Arnaud Giuliani — “Koin Compiler Plugin: New Features and Stable API”: The full Koin 4.1 release blog covers the stable compiler plugin API. The plugin now supports multi-module projects without manual module wiring, generates dependency graphs that can be visualized in Android Studio, and supports @Singleton alongside Koin’s native @Single.
Marcin Moskała — “Compose Modifier Ordering: The Drawing and Layout Phases”: A long-form exploration of modifier ordering using the decorator mental model. Key insight: modifiers apply from outermost to innermost for layout, but from innermost to outermost for drawing. This non-obvious behavior explains why padding().background() draws the background on the padded area, while background().padding() draws it on the full area.
“Room DAO Interface Inheritance for Shared Multi-Table Insert Logic”: Adam McNeilly’s article on using Room DAO interface inheritance to share multi-table insert logic across DAOs. The pattern: define a base BaseDao<T> interface with @Insert, @Update, and @Delete, then have specific DAOs extend it.
“Halogen — Generate Material 3 Themes at Runtime from Natural Language”: An overview of halogen, a Compose Multiplatform library that generates complete Material 3 themes at runtime from text prompts (“warm autumnal tones with high contrast”). Under the hood: the library uses Gemini Nano on-device to generate color palettes via the ML Kit Prompt API.
🛠️ Releases & Version Updates
Compose BOM 2026.04.00 (Stable): The April ‘26 stable BOM. Key library versions:
- Compose UI 1.9.0
- Material3 1.4.1
- Compose Runtime 1.9.0
- Compose Foundation 1.9.0
- Compose Animation 1.9.0
// build.gradle.kts — update to April '26 BOM
implementation(platform("androidx.compose:compose-bom:2026.04.00"))
BasicHtmlText — Now Stable:
// BasicHtmlText — render inline HTML without WebView
@Composable
fun HtmlContent(html: String) {
BasicHtmlText(
html = html, // Supports <b>, <i>, <u>, <a href>, <br>, <p>
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
onLinkClick = { url ->
uriHandler.openUri(url)
}
)
}
// Usage — useful for server-driven content with simple formatting
HtmlContent(
html = "Check out our <a href='https://mukuljangra.com'>blog</a> " +
"for <b>weekly Android updates</b>."
)
Android Studio Panda 4 (Stable): The major new AI features:
// Planning Mode — AI shows a plan before making changes
// Triggered via: Tools > Gemini > Planning Mode
// The AI outputs a structured plan:
// "I will:
// 1. Create UserRepository interface
// 2. Implement UserRepositoryImpl using Retrofit
// 3. Add Hilt bindings in NetworkModule
// 4. Update UserViewModel to inject UserRepository"
// Then executes the plan with your approval
// Next Edit Prediction — predict the developer's next change
// When you rename a variable, Studio predicts you also want to rename:
// - The ViewModel property
// - The composable parameter
// - Related test class fields
Koin 4.1.0 Stable: Compiler plugin is stable. Multi-module dependency graph visualization:
// koin-graph-viewer plugin — see your full DI graph in Android Studio
plugins {
id("io.insert-koin:koin-graph-viewer") version "4.1.0"
}
// Run: ./gradlew generateKoinGraph
// Opens in the Android Studio Dependency Analyzer
Halogen 1.0.0-beta01 (Compose Multiplatform):
// Generate Material 3 theme from a text prompt at runtime
@Composable
fun App() {
val themeGenerator = rememberHalogenThemeGenerator()
var theme by remember { mutableStateOf<HalogenTheme?>(null) }
LaunchedEffect(Unit) {
// Runs Gemini Nano on-device to generate a color palette
theme = themeGenerator.generateTheme(
prompt = "Warm, earthy tones with autumn colors, high contrast"
)
}
HalogenTheme(theme = theme) {
AppContent()
}
}
Compose April ‘26 — TextField Focus Management Fix
The overhauled TextField focus system in 1.9.0 fixes a long-standing bug where focus jumped unexpectedly in multi-field forms:
// Compose 1.9.0 — reliable focus management in multi-field forms
@Composable
fun LoginForm(onSubmit: (String, String) -> Unit) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
val focusManager = LocalFocusManager.current
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Down) }
)
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
onSubmit(email, password)
}
),
visualTransformation = PasswordVisualTransformation()
)
}
}
📱 Android Platform Updates
Room DAO Inheritance Pattern
The Adam McNeilly pattern that’s making the rounds:
// Base DAO with shared CRUD operations
interface BaseDao<T> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: T)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(entities: List<T>)
@Update
suspend fun update(entity: T)
@Delete
suspend fun delete(entity: T)
}
// Extend for specific entities — no boilerplate duplication
@Dao
interface UserDao : BaseDao<User> {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): User?
@Query("SELECT * FROM users")
fun observeAll(): Flow<List<User>>
}
@Dao
interface ProductDao : BaseDao<Product> {
@Query("SELECT * FROM products WHERE category = :category")
fun getByCategory(category: String): Flow<List<Product>>
}
Android 17 — Final Preparation Checklist
With Beta 4 being the last scheduled beta and GA approaching, here’s the final checklist:
- Test on Beta 4 emulator with
targetSdk = 37 - Verify
ACCESS_LOCAL_NETWORKpermission for LAN features - Validate all
System.load()calls use read-only native library paths - Check certificate chains are CT-logged for pinned connections
- Test all audio playback/focus flows in background state
- Verify large screen resizability doesn’t break portrait-locked screens
🌟 Community Spotlight
Halogen: A Compose Multiplatform library that generates Material 3 themes from natural language prompts using Gemini Nano on-device. This is the kind of creative library use case that shows what on-device AI makes possible for UX customization. Stars: 1.2k in its first week. GitHub: himattm/halogen.
Android Skills for LLM Agents: Google published an official repository of AI-optimized, modular Android development instructions for LLM-based coding agents. Think of it as the ultimate “system prompt” for getting an LLM to write idiomatic Kotlin/Compose code. Available at github.com/android/skills.
🧰 Tool of the Week
Pulsar — Haptic Feedback SDK: A cross-platform haptic feedback SDK for Android, iOS, and React Native with presets, a pattern composer, and real-time gesture-driven feedback. If you’ve ever wanted richer haptic feedback than the three system presets (CLICK, TICK, HEAVY_CLICK), Pulsar is worth exploring. Available at docs.swmansion.com/pulsar.
💡 Quick Tip of the Week
Use Modifier.onFocusedBoundsChanged in Compose 1.9 to anchor tooltips and menus to focused elements:
// Compose 1.9 — track focused element bounds for tooltip positioning
@Composable
fun TooltipField(
value: String,
tooltipText: String,
onValueChange: (String) -> Unit
) {
var focusedBounds by remember { mutableStateOf<Rect?>(null) }
var isFocused by remember { mutableStateOf(false) }
Box {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.onFocusChanged { isFocused = it.isFocused }
.onFocusedBoundsChanged { bounds -> focusedBounds = bounds }
)
// Tooltip anchored to focused bounds
if (isFocused && focusedBounds != null) {
Popup(
offset = IntOffset(
x = focusedBounds!!.left.roundToInt(),
y = (focusedBounds!!.bottom + 8).roundToInt()
)
) {
Surface(
shape = RoundedCornerShape(4.dp),
shadowElevation = 4.dp
) {
Text(tooltipText, modifier = Modifier.padding(8.dp))
}
}
}
}
}
🧠 Did You Know?
Compose’s BasicHtmlText (now stable) uses a custom AnnotatedString builder under the hood and doesn’t use WebView at all. This means it’s fully composable, respects Compose’s color system and typography, and participates in SelectionContainer for copy-paste. The supported HTML tags are deliberately limited (<b>, <i>, <u>, <a>, <br>, <p>, <small>, <big>) to avoid the complexity of full HTML rendering. For anything beyond basic inline formatting, you’d still reach for a WebView — but those tags cover the vast majority of rich-text server content use cases.
// Under the hood — what BasicHtmlText does internally
fun String.toAnnotatedHtml(): AnnotatedString = buildAnnotatedString {
// Parses HTML tags, builds SpannableString-equivalent AnnotatedString
// Respects currentTextStyle for unstyled segments
// Adds LinkAnnotation for <a href> elements
// Applies SpanStyle(fontWeight = FontWeight.Bold) for <b>
// No WebView, no WebKit, no Chromium — pure Compose
}
❓ Weekly Quiz
Q1: What is the Compose April ‘26 TextField focus management fix, and what class of bugs does it resolve?
A: The Compose 1.9.0 release overhauled TextField’s internal focus system, fixing bugs where focus jumped unexpectedly in multi-field forms (e.g., when a field is validated after onValueChange and the state update causes a recomposition that steals focus). The fix makes FocusDirection.Down and focusManager.clearFocus() work reliably across multi-field forms.
Q2: What is Android Studio Panda 4’s “Planning Mode” feature, and how does it differ from standard AI code generation?
A: Planning Mode shows a structured description of what the AI will do before making any changes. The developer reviews and approves the plan before execution. Standard code generation makes changes immediately — Planning Mode adds an explicit review step, reducing unwanted edits in large codebases.
Q3: What is the underlying technology powering Halogen’s runtime Material 3 theme generation?
A: Halogen uses Gemini Nano on-device via the ML Kit Prompt API to generate color palettes from natural language prompts. The generation runs locally without a network call, preserving user privacy and enabling offline theme generation.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Android 17 Beta 4: The Last Scheduled Beta”: Beta 4 is the final scheduled beta of the Android 17 release cycle — a critical milestone for app compatibility and platform stability testing. New items in Beta 4: app memory limits (targeted at extreme outliers), profiling triggers for anomaly detection, and Post-Quantum Cryptography (ML-DSA) in Android Keystore. SDK and game engine developers are urged to publish compatibility updates now.
Jaewoong Eum — “How Compose Hot Reload Works on Real Devices”: A detailed technical breakdown of the hot reload pipeline: incremental recompilation of the changed composable → class-file patching → ADB push → Compose’s ReloadScope API triggers re-execution. Supported changes: lambda bodies, string literals, numeric literals, and Color values. Unsupported: structural changes (adding/removing parameters, changing function signatures).
Thomas Ezan — “Experimental Hybrid Inference and New Gemini Models for Android”: Google introduced hybrid inference for Android — a mode where the inference request is evaluated on-device first (via Gemini Nano) and falls back to cloud (Gemini Pro/Flash) if the on-device model can’t handle the request. The switching is transparent to the app; you use the same GenerativeModel API for both paths.
“Android 17 Beta 4: Memory Limits and What They Mean for Your App”: A practical guide to Android 17’s new app memory limits. The limits are set conservatively in the initial release, targeting apps that exceed ~2GB of resident memory. The ApplicationExitInfo.description field will contain “MemoryLimiter” for affected exits. The recommended tool: LeakCanary’s new Android Studio Profiler integration.
“Post-Quantum Cryptography in Android Keystore”: Google’s security blog explains the new ML-DSA (Module-Lattice-Based Digital Signature Algorithm) support in Android Keystore. This is the NIST-standardized post-quantum signature algorithm. On supported devices, keys are generated and signatures computed entirely in secure hardware.
“Compose Recomposition as Test Expectations — A New Library”: An overview of a test-only library that turns Compose recomposition behavior into assertable, automatable test expectations. You can write assertions like assertThat(MyScreen).recomposedExactly(1).whenStateChanges(...) — precise recomposition testing without manual slot table inspection.
🛠️ Releases & Version Updates
Android 17 Beta 4: Available on all Pixel 6+ devices and the emulator. This is the near-final environment for testing. Key new APIs:
// Post-Quantum Cryptography — ML-DSA in Android Keystore
val keyPairGenerator = KeyPairGenerator.getInstance("ML-DSA-65", "AndroidKeyStore")
keyPairGenerator.initialize(
KeyGenParameterSpec.Builder(
"pqc-signing-key",
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).build()
)
val keyPair = keyPairGenerator.generateKeyPair()
// Sign data with the quantum-safe key
val signature = Signature.getInstance("ML-DSA-65")
signature.initSign(keyPair.private)
signature.update(dataToSign)
val signatureBytes = signature.sign()
// Verify signature
val verifySignature = Signature.getInstance("ML-DSA-65")
verifySignature.initVerify(keyPair.public)
verifySignature.update(dataToSign)
val isValid = verifySignature.verify(signatureBytes)
Gemini SDK for Android — Hybrid Inference (Experimental):
// Hybrid inference — on-device first, cloud fallback
dependencies {
implementation("com.google.ai.edge.aicore:aicore:1.0.0-alpha05")
}
// Single API, transparent on-device/cloud routing
val generativeModel = GenerativeModel(
modelName = "gemini-nano",
inferenceMode = InferenceMode.HYBRID, // NEW — tries on-device, falls back to cloud
apiKey = BuildConfig.GEMINI_API_KEY
)
val prompt = content { text("Summarize this in one sentence: $text") }
val response = generativeModel.generateContent(prompt)
println(response.text)
Compose 1.9.0-alpha03: Adds BasicHtmlText to stable (moved from experimental), improved SelectionContainer for multi-touch selection, and a new Modifier.onFocusedBoundsChanged for accessibility tools.
Koin 4.1.0-alpha04: The Koin Compiler Plugin stability improves — the alpha04 build handles circular dependency detection at compile time (previously only detected at runtime).
LeakCanary 3.0.0-alpha05: First alpha with Android Studio Profiler integration. LeakCanary detects heap leaks on-device and surfaces them directly in the Profiler’s LeakCanary task view, with source-code linkage.
Anomaly Detection with Android 17 ProfilingTrigger
// Register for anomaly-triggered profiling
val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java)
val triggers = listOf(
ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build()
)
val executor = Executors.newSingleThreadExecutor()
profilingManager.registerForAllProfilingResults(executor) { profilingResult ->
if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
// Heap dump collected automatically when memory limit is approached
uploadHeapDumpForAnalysis(profilingResult.resultFilePath)
}
}
profilingManager.addProfilingTriggers(triggers)
📱 Android Platform Updates
Android 17 — App Memory Limits
Android 17 introduces per-app memory limits based on device RAM. The conservative initial limits target extreme cases:
| Device RAM | Approximate App Memory Limit |
|---|---|
| 4 GB | ~1.5 GB |
| 6 GB | ~2.0 GB |
| 8 GB+ | ~2.5 GB |
The best practices: use ActivityManager.isLowRamDevice() for adaptive resource allocation, use the new ProfilingTrigger.TRIGGER_TYPE_ANOMALY for pre-crash heap dumps, and integrate LeakCanary in debug builds.
Compose Hot Reload — What Works, What Doesn’t
// ✅ Hot reload supported — immediate preview update on device
@Composable
fun WelcomeMessage() {
Text(
text = "Hello, Android 17!", // change text → hot reload
color = Color(0xFF6650A4), // change color → hot reload
fontSize = 24.sp // change size → hot reload
)
}
// ❌ Hot reload NOT supported — requires full recompile/redeploy
@Composable
fun WelcomeMessage(
name: String, // adding a parameter → not supported
modifier: Modifier = Modifier // adding modifier param → not supported
) {
// Structural changes require full recompile
}
🔒 Security & Vulnerability Alerts
Post-Quantum Cryptography Timeline: Google’s security team announced a roadmap to require PQC for high-value Android Keystore operations in a future Android release. Apps handling financial transactions, health data, or identity credentials should begin evaluating ML-DSA migration now. The hardware support is available on Pixel 9+ devices in the field today.
Certificate Transparency — Android 17 Default Behavior: CT is enabled by default for apps targeting Android 17. Before targeting API 37:
- Verify your server certificates are CT-logged (check with crt.sh)
- Test with
android:networkSecurityConfigpointing to a debug config that enables CT validation - Ensure any certificate-pinned connections use CAs that participate in CT
🎤 Conferences & Videos
Android Studio Profiler LeakCanary Integration Demo: Google published a demo video showing the new LeakCanary integration in Android Studio Panda’s profiler. You can trigger a heap analysis, see the leak tree, and jump directly to the leaking object’s creation site in your source code.
“Hybrid AI Inference for Android” — Google Developer Talk: Thomas Ezan’s talk explaining the hybrid inference architecture — how the SDK selects between on-device and cloud inference, how latency is managed, and the privacy model (on-device inference never leaves the device, cloud falls back with user-visible consent for sensitive requests).
💡 Quick Tip of the Week
Use ProfilingManager in debug builds to proactively capture performance data before issues are reported:
// In your Application class — debug builds only
if (BuildConfig.DEBUG) {
val profilingManager = getSystemService(ProfilingManager::class.java) ?: return
val executor = Executors.newSingleThreadExecutor()
profilingManager.requestProfiling(
ProfilingRequest.Builder(ProfilingType.JAVA_HEAP_DUMP)
.setSamplingIntervalBytes(512 * 1024) // 512KB
.build(),
executor
) { result ->
if (result.errorCode == ProfilingResult.ERROR_NONE) {
Log.d("Profiler", "Heap dump: ${result.resultFilePath}")
}
}
}
On Android 17, you can also register TRIGGER_TYPE_ANOMALY triggers to get automatic heap dumps when your app approaches memory limits — without waiting for a crash.
🧠 Did You Know?
Android’s new hybrid inference system doesn’t just route between on-device and cloud — it can split a single inference request. In some model architectures, the embedding computation can run on-device (cheap, private) while the decoding step runs in the cloud (powerful, higher quality). This split-inference approach is still experimental but represents a fundamentally different way to think about mobile AI:
// Hybrid inference modes (experimental)
val hybridModel = GenerativeModel(
modelName = "gemini-nano-hybrid",
inferenceMode = InferenceMode.HYBRID,
hybridConfig = HybridInferenceConfig.Builder()
.setOnDeviceFirst(true) // prefer on-device
.setCloudFallbackEnabled(true) // fall back to cloud for unsupported requests
.setMaxOnDeviceLatencyMs(500) // fall back if on-device takes >500ms
.build()
)
// The SDK handles routing transparently — same API call regardless of which path runs
val result = hybridModel.generateContent("What is the capital of France?")
The privacy model is clear: any request routed to cloud is flagged in the response metadata, so apps can show users when their data leaves the device.
❓ Weekly Quiz
Q1: What is the Compose Hot Reload pipeline on real devices, and which types of changes are supported?
A: The pipeline: incremental recompilation → class-file patching → ADB push → Compose ReloadScope re-execution. Supported changes: lambda bodies, string literals, numeric literals, color values. Unsupported: structural changes like adding/removing function parameters.
Q2: What is Post-Quantum Cryptography (ML-DSA), and why is it appearing in Android Keystore now?
A: ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is a NIST-standardized digital signature algorithm resistant to quantum computer attacks. It’s being added to Android Keystore now because current RSA/ECDSA keys are theoretically vulnerable to “harvest now, decrypt later” attacks using future quantum computers. Sensitive key material should start migrating.
Q3: What does hybrid inference in the Gemini SDK for Android do, and how is it configured?
A: Hybrid inference uses on-device Gemini Nano first and falls back to cloud (Gemini Pro/Flash) when the on-device model can’t handle the request. Set InferenceMode.HYBRID in GenerativeModel. The routing is transparent to the app — the same API call works regardless of which path executes.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Media3 1.10 Is Out”: The Media3 1.10 release adds Material 3 Compose playback widgets, a new Player composable combining video frame and controls, a ProgressSlider composable, PlaybackSpeedControl, and improved Transformer export speed. The media3-ui-compose-material3 module is maturing into a production-ready Compose media UI toolkit.
Jake Wharton — “Android KTX Libraries Are Being Retired”: Jake announced that the androidx.core:core-ktx, androidx.fragment:fragment-ktx, and related KTX libraries are being soft-retired. The Kotlin extensions they provided have been merged directly into their respective AndroidX libraries. core-ktx will continue to work as a shim but won’t receive new extensions.
Jaewoong Eum — “Compose Hot Reload on Real Devices: How It Works”: A detailed look at how Compose hot reload — which until recently only worked reliably on emulators — now works on physical Android devices. The article covers the compilation pipeline, the ADB-based class replacement mechanism, and the supported change types (lambda body changes, string literals, colors, dimensions).
“Media3 1.10: Building a Complete Media Player in Compose”: A tutorial building a full-featured video player using the new Player composable, ProgressSlider, and PlaybackSpeedToggleButton. The tutorial also covers handling MediaSessionService with LifecycleService (new in 1.10).
“Lottie Compose 6.6 — Multiplatform Animations”: With Lottie Compose 6.6 adding KMP support, the same Lottie animation files can now run on Android, iOS, and Desktop. The article benchmarks rendering performance across platforms and discusses strategies for reducing bundle size.
“KMP Network Inspector — Intercepting HTTP Without a Proxy”: An overview of a new KMP library that intercepts HTTP and WebSocket traffic without requiring a proxy setup. Works via OkHttp interceptors on Android and URLProtocol on iOS. Mock responses, throttle requests, and inspect headers in real time.
🛠️ Releases & Version Updates
Media3 1.10.0: The headline release. Here’s the new Player composable:
// Media3 1.10 — complete Player composable
dependencies {
implementation("androidx.media3:media3-ui-compose-material3:1.10.0")
implementation("androidx.media3:media3-exoplayer:1.10.0")
implementation("androidx.media3:media3-session:1.10.0")
}
@Composable
fun VideoPlayerScreen(videoUri: Uri) {
val context = LocalContext.current
val player = remember {
ExoPlayer.Builder(context)
.setHandleAudioBecomingNoisy(true)
.build()
.apply {
setMediaItem(MediaItem.fromUri(videoUri))
prepare()
playWhenReady = true
}
}
DisposableEffect(player) {
onDispose { player.release() }
}
// New in Media3 1.10 — single composable with built-in Material3 controls
Player(
player = player,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f),
showControls = true
)
}
// ProgressSlider — for custom player UIs
@Composable
fun CustomPlayerControls(player: Player) {
Column {
ProgressSlider(
player = player,
modifier = Modifier.fillMaxWidth()
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
PlaybackSpeedToggleButton(player = player)
PlayPauseButton(player = player)
SeekForwardButton(player = player)
}
}
}
AndroidX Core 1.16.0: Replaces core-ktx as the canonical library. All extension functions previously in core-ktx are now in core directly. The core-ktx artifact becomes a dependency shim that simply depends on core:
// Migration: remove -ktx suffix
// Old:
implementation("androidx.core:core-ktx:1.15.0")
// New (functionally identical — same extensions, same API):
implementation("androidx.core:core:1.16.0")
// Same for other KTX libraries:
// androidx.fragment:fragment-ktx → androidx.fragment:fragment
// androidx.activity:activity-ktx → androidx.activity:activity
// androidx.collection:collection-ktx → androidx.collection:collection
Coil 3.3.0: Adds AsyncImage support for BlurTransformation and GrayscaleTransformation without requiring custom ImageRequest.Builder modifications. Also adds a CoilPainter.isLoading property for driving loading indicator state.
Detekt 1.24.1: Hotfix for a false positive in ComposeRememberMissing that flagged rememberSaveable incorrectly. If you’re on 1.24.0, upgrade.
Media3 1.10 — MediaSessionService with LifecycleService
// Media3 1.10 — MediaSessionService now extends LifecycleService
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
// Access lifecycle scope — new in 1.10
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Collect from flows tied to service lifecycle
userPreferences.repeatMode.collect { mode ->
player.repeatMode = mode
}
}
}
mediaSession = MediaSession.Builder(this, player).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
}
📱 Android Platform Updates
Android KTX Deprecation — What It Means Practically
The KTX libraries aren’t being removed — they’ll continue working as dependency shims. But they won’t receive new Kotlin extensions. The reason: as Jetpack libraries have become Kotlin-first, the “Kotlin extensions” layer is now redundant. The AndroidX libraries themselves are written in Kotlin and expose idiomatic Kotlin APIs directly.
Timeline:
- Now: KTX libraries are shims pointing to their base library
- Future: KTX artifacts may be removed entirely from new project templates
- Never (likely): The
-ktxartifacts will remain on Maven for backward compatibility
Android 17 — Background Audio Hardening Details
Google clarified the Android 17 background audio restrictions following developer feedback:
- Audio focus requests from background apps require an active foreground service
- Volume change APIs (
adjustStreamVolume) blocked for background apps without active audio session - Exemptions: alarm audio, system audio, apps with active
MediaSession - targetSdk gating: enforcement only for apps targeting API 37+
⚠️ Deprecation & Migration Alerts
-ktx Libraries — Migration Timeline: No immediate action required. The -ktx libraries continue to work. When starting new modules or updating existing dependencies, prefer the base library directly (core instead of core-ktx). Gradle will warn you when you depend on a deprecated KTX shim in AGP 9.0+.
Media3 FrameExtractor Moved: FrameExtractor moved from media3-inspector to the new media3-inspector-frame module. Update your imports:
// Old:
import androidx.media3.inspector.FrameExtractor
// New:
import androidx.media3.inspector.frame.FrameExtractor
// build.gradle.kts
implementation("androidx.media3:media3-inspector-frame:1.10.0")
💡 Quick Tip of the Week
Use MediaItem.ClippingConfiguration in Media3 to play only a portion of a media file:
// Play only seconds 30–90 of a video
val clippedMediaItem = MediaItem.Builder()
.setUri(videoUri)
.setClippingConfiguration(
MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(30_000L)
.setEndPositionMs(90_000L)
.setRelativeToDefaultPosition(false)
.build()
)
.build()
player.setMediaItem(clippedMediaItem)
player.prepare()
player.play()
This works on any ExoPlayer-supported format and doesn’t require pre-processing the file. Great for video trim previews and audio clip players.
🧠 Did You Know?
core-ktx is over 5 years old and has been providing Kotlin extension functions since before Jetpack libraries were written in Kotlin. When Jake Wharton announced the KTX retirement, many developers were surprised to learn that the extensions were so widely used. Looking at adoption metrics, core-ktx was imported by more than 85% of all published Android apps. The most used extensions: Bundle.putString, Context.systemService<T>(), View.doOnLayout, and String.toUri(). All of these are now in core directly — no migration needed beyond the dependency declaration.
// These still work exactly the same — just from core instead of core-ktx
val notificationManager = context.getSystemService<NotificationManager>()
val uri = "https://mukuljangra.com".toUri()
view.doOnLayout { view ->
val height = view.measuredHeight
}
val bundle = bundleOf(
"userId" to userId,
"source" to "notification"
)
❓ Weekly Quiz
Q1: What is the new Player composable in Media3 1.10, and what does it combine?
A: The Player composable combines a ContentFrame (renders video) with customizable Material 3 playback controls. It’s an out-of-the-box player widget with a modern UI that replaces the older PlayerView (View-based) for Compose apps.
Q2: Why did Jake Wharton announce the Android KTX libraries are being retired?
A: Because Jetpack libraries themselves are now written in Kotlin and provide idiomatic Kotlin APIs directly. The KTX “extension” layer has become redundant — there’s no longer a need for separate extension functions on top of Java-based libraries. KTX continues to work as a shim but won’t receive new extensions.
Q3: What change does Media3 1.10 make to MediaSessionService that enables better lifecycle management?
A: MediaSessionService now extends LifecycleService, giving the service access to lifecycleScope and repeatOnLifecycle. This allows the service to collect from Flows and tie async work to the service’s lifecycle, replacing manual lifecycle management.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Android 17 Beta 3: Platform Stability Reached”: Beta 3 marks the API lock milestone for Android 17. All SDK and NDK APIs are now final. Apps can target compileSdk = 37 and publish to Google Play immediately. Google confirmed the behavior changes that will be enforced for apps targeting API 37: large screen resizability restrictions, safer dynamic code loading extension to native libs, and Certificate Transparency by default.
CommonsWare — “The March 25 Jetpack Artifact Wave”: Mark Murphy documents the massive Jetpack release wave from March 25. Highlights: Room 3.0 gains Android TV and Automotive OS targets, a new wear-compose-remote artifact enables phone-rendered Compose on watches, and several AndroidX alpha libraries bumped to beta. The Room multiplatform expansion is the most significant.
“Android 17 App Compatibility Checklist — Resizability on Large Screens”: Once an app targets API 37, it can no longer opt out of maintaining orientation, resizability, and aspect ratio constraints on large screens. This affects apps that use android:screenOrientation="portrait" — on tablets and foldables, the system will ignore this constraint.
“Certificate Transparency in Android 17 — What Changes”: CT is enabled by default in Android 17 (opt-in in Android 16). Apps pinning certificates via Network Security Config need to verify their CA chains are CT-logged. The article explains how to test CT compliance with curl and openssl before targeting API 37.
“Building with Navigation 3 in Production”: A case study from a mid-size app team that migrated from Navigation 2 to Navigation 3. The migration took 2 sprints. The wins: type-safe routes, predictive back integration, and shared element transitions. The challenges: bottom tab state preservation required custom back stack management.
“Kotlin Multiplatform Statistics Toolkit”: A new KMP library (kotlin-statistics) covering distributions, hypothesis testing, descriptive statistics, sampling, and correlation analysis. Useful for apps doing on-device analytics, health metric calculations, or recommendation engine features.
🛠️ Releases & Version Updates
AndroidX Compose BOM 2026.04.00-alpha01: First alpha of the April BOM cycle includes Compose 1.9.0-alpha02 with improved LazyColumn prefetch that accounts for scroll velocity, reducing visible item pop-in on fast flings.
Room 3.0.0-alpha02: New multiplatform targets and fixes for the initial alpha:
// Room 3.0 — expanded KMP targets
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
macosArm64()
tvosArm64() // NEW in alpha02
watchosArm64() // NEW in alpha02
jvm("desktop") // NEW in alpha02
sourceSets {
commonMain.dependencies {
implementation("androidx.room:room-runtime:3.0.0-alpha02")
implementation("androidx.sqlite:sqlite-bundled:2.5.0")
}
androidMain.dependencies {
implementation("androidx.sqlite:sqlite-android:2.5.0")
}
}
}
// Same DAO, works on all targets
@Dao
interface ProductDao {
@Query("SELECT * FROM products WHERE category = :category")
fun getByCategory(category: String): Flow<List<Product>>
@Upsert
suspend fun upsert(product: Product)
}
Hilt 2.56.1: Fixes a crash when using @HiltViewModel with Navigation 3’s BackStackEntry as the ViewModelStoreOwner.
Accompanist 0.37.0 (Final): This is it — the last release. If you still have Accompanist dependencies, now is the time to migrate:
// Migration guide — common Accompanist → AndroidX mappings
// Pager
// ❌ com.google.accompanist:accompanist-pager
// ✅ androidx.compose.foundation:foundation (HorizontalPager, VerticalPager)
// SystemUIController
// ❌ com.google.accompanist:accompanist-systemuicontroller
// ✅ enableEdgeToEdge() in ComponentActivity
// FlowLayout
// ❌ com.google.accompanist:accompanist-flowlayout
// ✅ androidx.compose.foundation:foundation (FlowRow, FlowColumn)
// Placeholder
// ❌ com.google.accompanist:accompanist-placeholder
// ✅ Custom shimmer effect (see below)
// Shimmer placeholder without Accompanist
@Composable
fun ShimmerPlaceholder(
isLoading: Boolean,
content: @Composable () -> Unit
) {
if (isLoading) {
val shimmerColors = listOf(
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
)
val transition = rememberInfiniteTransition(label = "shimmer")
val translateAnim by transition.animateFloat(
initialValue = 0f, targetValue = 1000f,
animationSpec = infiniteRepeatable(tween(1200), RepeatMode.Restart),
label = "shimmer_translate"
)
Box(
modifier = Modifier
.background(Brush.linearGradient(shimmerColors, start = Offset(translateAnim - 200f, 0f), end = Offset(translateAnim, 0f)))
)
} else {
content()
}
}
Android 17 Beta 3 — Behavior Changes Summary
// 1. Dynamic code loading restriction — native libs must be read-only
// This WILL throw UnsatisfiedLinkError in Android 17+ if lib is writable:
System.load("/data/data/com.example/files/myplugin.so")
// Fix: load only from read-only paths (installed APK paths are fine)
// 2. Local network permission — required for LAN discovery
// Declare in manifest:
// <uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
// Request at runtime like any other permission
val granted = ActivityCompat.checkSelfPermission(
context, "android.permission.ACCESS_LOCAL_NETWORK"
) == PackageManager.PERMISSION_GRANTED
// 3. Large screen resizability — can no longer opt out on API 37+
// Remove or stop relying on these:
// android:screenOrientation="portrait" // Ignored on large screens
// android:resizeableActivity="false" // Ignored on large screens
📱 Android Platform Updates
Memory Limits in Android 17
Android 17 introduces per-app memory limits based on device RAM. The limits are conservative in the initial release — targeting only extreme outliers. Apps can detect if they hit a limit via ApplicationExitInfo:
fun checkForMemoryLimitExits(context: Context) {
val activityManager = context.getSystemService(ActivityManager::class.java)
val exitReasons = activityManager.getHistoricalProcessExitReasons(
context.packageName,
0, // most recent
5 // last 5 exits
)
exitReasons.filter { exitInfo ->
exitInfo.description?.contains("MemoryLimiter") == true
}.forEach { exitInfo ->
// App was killed due to memory limit — log for crash reporting
analytics.logEvent("memory_limit_exit", mapOf(
"process" to exitInfo.processName,
"timestamp" to exitInfo.timestamp
))
}
}
⚠️ Deprecation & Migration Alerts
Accompanist is officially retired. All APIs have been migrated to official AndroidX libraries. Check your build.gradle.kts files for any remaining com.google.accompanist: dependencies and migrate them before the library is removed from repositories.
Navigation 2.x — No New Features: Google confirmed that Navigation 2 will continue to receive bug fixes but no new features. Navigation 3 is the active development branch. Migration guides are available at developer.android.com.
💡 Quick Tip of the Week
Use Modifier.composed {} when you need to read Compose state inside a Modifier:
// ❌ Reading state inside a regular Modifier is not stable
fun Modifier.dynamicBorder(colorState: State<Color>): Modifier =
border(2.dp, colorState.value) // colorState.value read outside composition
// ✅ Modifier.composed — reads state inside composition safely
fun Modifier.dynamicBorder(colorState: State<Color>): Modifier =
composed {
val color by colorState
border(2.dp, color, RoundedCornerShape(8.dp))
}
// Even better: use a @Composable function to create the modifier
@Composable
fun Modifier.highlightWhenFocused(): Modifier {
val isFocused by remember { mutableStateOf(false) }
return this.then(
if (isFocused) Modifier.border(2.dp, MaterialTheme.colorScheme.primary)
else Modifier
)
}
🧠 Did You Know?
Flow.flatMapLatest cancels the previous inner flow, but Flow.flatMapMerge doesn’t — and this difference causes silent data loss in search features. Most search implementations want flatMapLatest (cancel the old search when the user types again), but many developers accidentally use flatMapConcat or flatMapMerge:
class SearchViewModel : ViewModel() {
private val queryFlow = MutableStateFlow("")
// ❌ flatMapConcat: queues every keystroke — old results arrive after new ones
val results1 = queryFlow.flatMapConcat { query -> searchApi.search(query) }
// ❌ flatMapMerge: all searches run in parallel — results arrive out of order
val results2 = queryFlow.flatMapMerge { query -> searchApi.search(query) }
// ✅ flatMapLatest: cancels previous search when a new query arrives
val results3 = queryFlow.flatMapLatest { query ->
if (query.isEmpty()) flowOf(emptyList())
else searchApi.search(query)
}
}
flatMapLatest is the correct operator for any “latest value wins” scenario: search, location tracking with debounce, and live filter inputs.
❓ Weekly Quiz
Q1: What does Android 17 Beta 3’s “platform stability” milestone mean in practical terms for app developers who want to publish to Google Play?
A: Apps can now target compileSdk = 37 and publish to Google Play immediately. The API surface is final — no more breaking changes. Developers can complete their API 37 compatibility testing with confidence that the APIs they’re testing against are production-ready.
Q2: Why does targeting Android 17 (API 37) change behavior on large screens regarding android:screenOrientation="portrait"?
A: Apps targeting API 37 can no longer opt out of maintaining orientation, resizability, and aspect ratio constraints on large screens. The system ignores screenOrientation and resizeableActivity="false" on tablets, foldables, and other large-screen devices to ensure a consistent, adaptive user experience.
Q3: What is the difference between flatMapLatest, flatMapConcat, and flatMapMerge in Kotlin Flow for a search feature?
A: flatMapLatest cancels the previous search when a new query arrives (correct for search). flatMapConcat queues searches sequentially (results can arrive out of order, old results block new ones). flatMapMerge runs all searches in parallel (results arrive out of order, no cancellation).
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
JetBrains — “Kotlin 2.3.20 Released”: The 2.3.20 maintenance release ships Gradle 9.3 compatibility, name-based destructuring declarations, a new C/Objective-C interop mode for Kotlin/Native, and improved Maven setup for KMP projects. The Gradle 9.3 compatibility is the most operationally urgent change — teams on Gradle 9.3+ will need this version.
Google — “Get Inspired and Take Your Apps to Desktop”: Google published extensive desktop design guidance and launched the Android Design Gallery. This accompanies a separate announcement that Android apps can now extend to external monitors via DisplayLink on Android 14+ devices. Apps in desktop mode run at full display resolution with keyboard and mouse support.
“Kotlin 2.3.20: Name-Based Destructuring in Practice”: A deep-dive into the new name-based destructuring declaration, which lets you destructure by property name rather than position. The key improvement: data classes with many fields no longer require you to count positions, and adding a field in the middle no longer silently shifts all destructuring assignments.
“CommonsWare: March 25 Jetpack Artifact Wave”: Mark Murphy (CommonsWare) documents the unusually large March 25 Jetpack release wave — Room 3.0 new multiplatform targets, Wear Compose Remote, and several AndroidX alpha bumps. The Room 3.0 multiplatform additions are the highlight.
“Tracey — Compose Flight Recorder for Android, iOS, and Desktop”: The open-source KMP library for recording app sessions as structured event logs, with a browser-based replay viewer. The library hooks into Compose’s accessibility tree to capture interactions without requiring manual instrumentation.
“Navigating the Compose Navigation 3 Alpha with Pure Reducers”: A walkthrough of a community navigation library built around immutable state and pure reducers. The approach: navigation state is a plain List<Screen>, every navigation action is a pure function, and the library handles the NavHost rendering. Highly testable, predictable, and debuggable.
🛠️ Releases & Version Updates
Kotlin 2.3.20: Key changes for Android developers:
// New: Name-based destructuring declarations
data class User(val id: String, val name: String, val email: String, val age: Int)
// Old positional destructuring — fragile when fields are added/reordered
val (id, name, _, age) = user // What is _? You have to count.
// New: Name-based — explicit and refactoring-safe
val (id, name, age) by user // Uses property names, order doesn't matter
// email is simply not destructured — no placeholder needed
// Gradle 9.3 compatibility — update your Kotlin version
// build.gradle.kts
plugins {
kotlin("android") version "2.3.20"
// Gradle 9.3 compatibility requires 2.3.20+
}
Compose BOM 2026.03.00: Stable release including Compose Runtime 1.8.0 stable, Compose UI 1.8.0 stable, and Material3 1.4.0 stable. The runtime stable release means all @Stable compiler analysis improvements are now production-ready.
Koin 4.0.4: Maintenance release fixing koinApplication {} builder thread-safety in multi-threaded initialization scenarios (affects apps that initialize Koin from background threads via WorkManager).
AGP 8.9.0-beta01: Beta release with R8 Full Mode enabled by default for new modules. Existing modules need to opt in via isMinifyEnabled = true + proguardFiles(...). The beta also fixes a long-standing issue where Compose preview rendering in Studio would fail if any KSP-generated code was in the dependency graph.
Kotlinx.coroutines 1.10.2: Adds Flow.onEmpty {} operator — fires when a Flow completes without emitting any values. Useful for showing empty state UI in collectLatest patterns:
viewModel.items
.onEmpty { showEmptyState() }
.onEach { items -> showContent(items) }
.launchIn(lifecycleScope)
Kotlin 2.3.20 — C/Objective-C Interop Mode
For KMP projects targeting iOS/macOS, the new cinterop mode improves type bridging:
// Kotlin/Native interop with Objective-C — improved in 2.3.20
// cinterop generates cleaner Kotlin stubs for Objective-C generics
// Previously: ObjC NSArray<NSString *> became Array<Any?>
// Now: NSArray<NSString *> becomes Array<String>
fun processStrings(array: NSArray<NSString>) {
array.forEach { str ->
println(str) // str is now String, not Any?
}
}
📱 Android Platform Updates
Android 17 Beta 3 — Platform Stability Milestone
Android 17 Beta 3 is expected this week, marking the API lock. From this point:
- No new APIs will be added to the Android 17 SDK
- Apps can target
compileSdk = 37and publish to Google Play for early feedback - The focus shifts to stability, performance, and behavior change validation
Apps should prioritize testing these Android 17 behavior changes:
ACCESS_LOCAL_NETWORKpermission required for LAN device discovery- Background audio focus restrictions
- Dynamic code loading restrictions (native libraries must be read-only)
- Certificate Transparency enabled by default
Android Apps on External Monitors
The DisplayLink integration in Android 14+ (QPR1) is now officially supported for production use. Apps that support android:resizeableActivity="true" get external monitor support automatically. Apps that want to optimize for desktop mode should implement:
// Detect external display mode
val displayManager = context.getSystemService(DisplayManager::class.java)
val displays = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
if (displays.isNotEmpty()) {
// App is on an external display — optimize for desktop
enableDesktopFeatures()
}
// Observe display changes
displayManager.registerDisplayListener(object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) { checkDisplayMode() }
override fun onDisplayRemoved(displayId: Int) { checkDisplayMode() }
override fun onDisplayChanged(displayId: Int) { checkDisplayMode() }
}, null)
🌟 Community Spotlight
Tracey: An open-source Compose flight recorder for Android, iOS, and Desktop. Records gestures, breadcrumbs, screen views, and crashes with timestamps. The data is stored locally and can be exported as structured JSON or replayed in the browser-based viewer. It’s the closest thing to Amplitude’s session replay for native apps. GitHub: hi-manshu/tracey.
A Browser-Based Android Lint Playground: A community-built tool that lets you write, test, and share custom Android Lint rules with live SARIF output — no local Android Studio required. Useful for developing lint rules for your design system or architecture guidelines.
🧰 Tool of the Week
Android Virtual Device Menu Bar App (macOS): A macOS menu bar app for quickly launching and managing Android Virtual Devices without opening Android Studio. Supports filtering by API level, starting/stopping devices, and deep-linking directly into running emulators. Available on GitHub. If you run multiple emulators daily, the time savings add up.
💡 Quick Tip of the Week
Kotlin’s buildString is faster than string concatenation for multi-step string construction:
// ❌ String concatenation — allocates intermediate String objects
fun formatUserInfo(user: User): String {
var result = "Name: ${user.name}"
result += "\nEmail: ${user.email}"
result += "\nJoined: ${user.joinDate}"
if (user.isPremium) result += "\n⭐ Premium member"
return result
}
// ✅ buildString — single StringBuilder, zero intermediate allocations
fun formatUserInfo(user: User): String = buildString {
append("Name: ").appendLine(user.name)
append("Email: ").appendLine(user.email)
append("Joined: ").appendLine(user.joinDate)
if (user.isPremium) appendLine("⭐ Premium member")
}
buildString uses a StringBuilder internally, so it’s particularly valuable in hot paths like RecyclerView adapters, list formatters, or logging utilities.
🧠 Did You Know?
Kotlin’s by keyword in destructuring and delegation look identical but are completely different features. Name-based destructuring (new in 2.3.20) uses val (name, age) by user, where by invokes the component() functions by name. Class delegation (class A : B by b) uses by to forward interface implementation. Property delegation (val x by lazy { }) uses by to invoke the getValue/setValue operators. All three share the same keyword but have zero semantic overlap:
// 1. Property delegation — operator overloading (getValue/setValue)
val config by lazy { loadConfig() }
var count by mutableStateOf(0)
// 2. Class delegation — interface forwarding
class LoggingList<T>(private val delegate: List<T>) : List<T> by delegate {
override fun get(index: Int): T {
println("get($index)")
return delegate[index]
}
}
// 3. Name-based destructuring — component functions by name (NEW in 2.3.20)
data class Point(val x: Int, val y: Int, val z: Int)
val (x, z) by Point(1, 2, 3) // y is not destructured — no placeholder
❓ Weekly Quiz
Q1: What is the primary operational advantage of name-based destructuring over positional destructuring in Kotlin?
A: Name-based destructuring is refactoring-safe. Adding a field to a data class in the middle no longer silently shifts all positional destructuring assignments, preventing subtle bugs. Unused fields don’t require placeholder underscores.
Q2: What does Android 17 Beta 3’s “platform stability” milestone mean for app developers?
A: The SDK/NDK API surface is locked. No new APIs will be added. Apps can now target compileSdk = 37, publish to Google Play for testing, and complete their compatibility validation knowing the APIs they test against are final.
Q3: What is Flow.onEmpty {} in kotlinx.coroutines 1.10.2 useful for in Android development?
A: It fires when a Flow completes without emitting any values. This is useful for showing an empty state in UI — when a database query returns no rows or an API returns an empty list, onEmpty lets you show the empty state view without a special null or empty list check.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Marcello Galhardo — “New Lifecycle ViewModel APIs: Scoping Beyond Navigation”: The stable Lifecycle 2.9 release formalizes the ability to scope ViewModels to arbitrary positions in the Compose hierarchy. The article shows three patterns: screen-level scoping (traditional), component-level scoping (new), and subtree scoping for feature modules. The key API: viewModel(viewModelStoreOwner = LocalViewModelStoreOwner.current) with a custom ViewModelStoreOwner provider.
Thomas Künneth — “KMP Navigation: Decompose, Circuit, and Compose Multiplatform”: A thorough comparison of navigation approaches for Kotlin Multiplatform apps. Decompose favors a component model with explicit back-stack management; Circuit (Slack) favors a unidirectional data flow with presenter-based navigation; Compose Multiplatform’s built-in navigation is the simplest but least powerful. The article recommends Circuit for greenfield KMP projects.
“Android Desktop Experience Design Guidance Published”: Google published comprehensive design guidance for “desktop mode” on Android — when apps run on external monitors via DisplayLink or on ChromeOS in windowed mode. Key principles: higher information density, keyboard/mouse input handling, window resizing to arbitrary sizes, and taskbar integration. A new Android Design Gallery was launched alongside.
“Compose Recomposition Monitoring in Production”: A look at using Compose’s RecompositionTracker API in debug builds to establish recomposition baselines and flag regressions in CI. The article shows how to pipe recomposition stats into a custom analytics dashboard.
“Kotlin Multiplatform for Ticket Validation — A Masabi Case Study”: Paweł Kwieciński from Masabi explains how they brought a 10-year-old Java codebase to KMP, powering ticket validation across mobile apps, embedded devices, and backend systems. The key challenge: Kotlin/Native performance on resource-constrained embedded devices. Their solution: @OptIn(ExperimentalNativeApi::class) carefully applied, with native memory management annotations.
“R8 Full Mode in AGP 8.9 — A Complete Migration Guide”: AGP 8.9 enables R8 Full Mode by default for new projects. The guide covers common breakage patterns: reflection-based serialization, class name-based routing, Gson model classes, and Retrofit interface implementation. The fix toolkit: @Keep, -keepclassmembers, and @JsonClass(generateAdapter = true) for Moshi.
🛠️ Releases & Version Updates
Lifecycle 2.9.0 Stable: The viewModel() composable in lifecycle-viewmodel-compose now accepts a custom viewModelStoreOwner and factory parameter:
// Create a custom ViewModelStoreOwner for fine-grained ViewModel scoping
class CompositionViewModelStoreOwner : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
fun clear() = viewModelStore.clear()
}
// Scope a ViewModel to a specific composable subtree
@Composable
fun ProductFeature(productId: String) {
// ViewModelStoreOwner lives as long as this composable is in the tree
val vmStoreOwner = remember(productId) { CompositionViewModelStoreOwner() }
DisposableEffect(productId) {
onDispose { vmStoreOwner.clear() }
}
val vm = viewModel<ProductViewModel>(
viewModelStoreOwner = vmStoreOwner,
factory = ProductViewModel.Factory(productId)
)
ProductContent(vm.uiState.collectAsStateWithLifecycle().value)
}
Compose Multiplatform 1.8.0: Adds iOS-native text input handling (first-class IMEOptions on iOS), improved Modifier.pointerInput on Desktop, and a new LocalWindowInfo.current.isKeyboardVisible API shared across platforms:
// Shared Compose Multiplatform code — keyboard visibility across platforms
@Composable
fun AdaptiveForm() {
val keyboardVisible = LocalWindowInfo.current.isKeyboardVisible
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(bottom = if (keyboardVisible) 300.dp else 16.dp)
) {
FormFields()
}
}
Detekt 1.24.0: Adds new rules for Compose — ComposeComposableModifier (enforces that modifier parameters use Modifier = Modifier default), ComposeNamingUppercase (enforces @Composable functions start with uppercase), and ComposeRememberMissing (flags state creation without remember).
OkHttp 5.0.0-alpha17: Continues the alpha series with improved HTTP/3 (QUIC) support and a new EventListener API that reports request/response timing at the frame level for HTTP/2 and HTTP/3.
Android Desktop Design — Key Patterns
// Detect if app is in desktop/windowed mode
@Composable
fun adaptToWindowMode(): WindowMode {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val configuration = LocalConfiguration.current
return when {
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) &&
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED ->
WindowMode.Desktop
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED ->
WindowMode.Tablet
else -> WindowMode.Phone
}
}
@Composable
fun AppLayout() {
when (adaptToWindowMode()) {
WindowMode.Desktop -> DesktopLayout() // High density, menu bar
WindowMode.Tablet -> TabletLayout() // Navigation rail
WindowMode.Phone -> PhoneLayout() // Bottom navigation
}
}
📱 Android Platform Updates
Android 17 — API Lock Approaching
Beta 3 is expected this week or next, marking platform stability. After platform stability:
- Final SDK/NDK APIs are locked
- Apps can target SDK 37 and publish to Google Play
- No more breaking API changes in remaining betas
ChromeOS + Android Desktop Mode
Google announced that Android apps can seamlessly extend to external monitors via DisplayLink, enabling a true desktop-class experience. The new guidance covers:
- Window resizing to arbitrary dimensions
- Keyboard shortcut registration via
KeyboardShortcutGroup - Context menu support for right-click events
- Drag-and-drop between apps
// Register keyboard shortcuts for desktop mode
override fun onProvideKeyboardShortcuts(
data: MutableList<KeyboardShortcutGroup>,
menu: Menu?,
deviceId: Int
) {
val shortcuts = KeyboardShortcutGroup(
"File operations",
listOf(
KeyboardShortcutInfo("Save", KeyEvent.KEYCODE_S, KeyEvent.META_CTRL_ON),
KeyboardShortcutInfo("New", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON),
KeyboardShortcutInfo("Open", KeyEvent.KEYCODE_O, KeyEvent.META_CTRL_ON)
)
)
data.add(shortcuts)
}
🎤 Conferences & Videos
“R8 Deep Dive” — The Modern Android Development Podcast: Hosts Tor and Romain sit with Søren Gjesse, Chris Craik, and Shai Barack to discuss how R8 differs from ProGuard, what Full Mode enables, and the performance implications of different optimization strategies. Essential listening before enabling R8 Full Mode in your project.
Talking Kotlin — “KMP in Production: Masabi’s Journey”: Paweł Kwieciński discusses the Masabi KMP migration in podcast form. The most interesting segment: how they handle Kotlin/Native memory management on embedded ticket validators with ~128MB RAM.
💡 Quick Tip of the Week
Use WindowInsets.are APIs to handle system bars in Compose without manual insets calculation:
// ❌ Manual insets calculation — fragile across API levels
@Composable
fun Screen() {
val insets = WindowInsetsCompat.toWindowInsetsCompat(
LocalView.current.rootWindowInsets
)
val topPadding = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
Box(modifier = Modifier.padding(top = topPadding.dp)) { Content() }
}
// ✅ WindowInsets DSL — handles all edge cases automatically
@Composable
fun Screen() {
Scaffold(
// Scaffold handles status bar and navigation bar padding
topBar = { TopAppBar(title = { Text("Title") }) },
content = { padding ->
Content(modifier = Modifier.padding(padding))
}
)
}
// For custom layouts without Scaffold:
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) { Content() }
🧠 Did You Know?
Compose Modifier.pointerInput has a subtle restart behavior that causes animation glitches. When any key passed to pointerInput changes, the gesture detector is restarted — cancelling any in-progress gestures. This means passing a lambda as a key (which creates a new instance every recomposition) restarts gesture detection on every recomposition:
// ❌ Lambda as key — pointerInput restarts on every recomposition
Modifier.pointerInput(onSwipe) { // onSwipe is a lambda — new instance each time
detectHorizontalDragGestures { _, dragAmount -> onSwipe(dragAmount) }
}
// ✅ Unit as key with rememberUpdatedState — stable key, always-fresh callback
@Composable
fun SwipeableBox(onSwipe: (Float) -> Unit, content: @Composable () -> Unit) {
val currentOnSwipe by rememberUpdatedState(onSwipe)
Box(
modifier = Modifier.pointerInput(Unit) { // Unit = never restart
detectHorizontalDragGestures { _, dragAmount -> currentOnSwipe(dragAmount) }
}
) { content() }
}
This is a well-known footgun for developers moving from Compose basics to gesture-heavy UIs.
❓ Weekly Quiz
Q1: In Lifecycle 2.9, what is the recommended way to scope a ViewModel to a specific Compose subtree rather than the entire Activity?
A: Create a custom ViewModelStoreOwner (e.g., CompositionViewModelStoreOwner) backed by a ViewModelStore, pass it to viewModel(viewModelStoreOwner = ...), and clear it in a DisposableEffect. This scopes the ViewModel lifetime to the composable’s presence in the composition tree.
Q2: What is the risk of passing a lambda as a key to Modifier.pointerInput?
A: Lambda instances are new objects on every recomposition, so pointerInput with a lambda key restarts on every recomposition — cancelling any in-progress gestures. Use Unit as the key combined with rememberUpdatedState to keep the callback fresh without restarting.
Q3: When designing for Android desktop mode, what input states require special consideration beyond touch interactions?
A: Keyboard shortcuts (registered via onProvideKeyboardShortcuts), mouse hover states, right-click context menus, pointer cursor icon changes, and drag-and-drop between apps. Touch-first designs assume tap targets — desktop mode requires handling keyboard and mouse precision inputs that have no touch equivalent.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Room 3.0 Alpha: Kotlin Multiplatform, Coroutines-First”: Room 3.0 drops Java annotation-based code generation in favor of a pure Kotlin KSP pipeline and adds Kotlin Multiplatform targets (iOS, macOS, Windows). The biggest architectural change: all DAO methods must be suspend or return Flow — no blocking Room operations in 3.0. Java LiveData-returning DAOs are removed entirely.
Kirill Rozov — “Detekt Rules for Koin: Enforcing DI Best Practices”: A ready-to-use Detekt plugin with rules for Koin usage: detecting module declarations with too many bindings, get() calls in constructors (which bypass constructor injection), and missing @KoinInternalApi annotations on internal Koin utilities. The plugin runs in CI and integrates with the existing Detekt config.
“Remote Compose — Serving UI From a Server Without JSON or WebViews”: Arman Chatikyan’s deep dive into Google’s Remote Compose protocol. The system serializes Compose layout descriptions to a binary format, sends them over the network, and renders them on-device using real Compose. Current limitations: no arbitrary lambdas in server-rendered composables, event handling requires pre-registered interaction IDs.
“ViewModel Scoping to Compose UI Hierarchy in Lifecycle 2.9”: Marcello Galhardo’s follow-up article with code examples from the stable Lifecycle 2.9 APIs. The viewModel() composable now accepts a viewModelStoreOwner parameter to scope ViewModel lifetime to any level of the Compose hierarchy.
“A Gradle Plugin for Deep Link Routing”: An open-source Gradle plugin that matches URL patterns to Navigation destinations at build time, generating type-safe deep link handlers instead of manual intent-filter XML + NavDeepLinkBuilder combinations.
“Compose Recomposition — Budget-Based Monitoring with Zero Config”: An overview of a new debug library that tracks recomposition rates and flags composables exceeding platform-appropriate budgets (3/s for screens, 120/s for animations). The library is debug-only and uses Compose’s RecompositionTracker API.
🛠️ Releases & Version Updates
Room 3.0.0-alpha01: The first alpha of the multiplatform Room rewrite. Here’s what the migration looks like:
// Room 3.0 — pure KSP, no Java codegen
// build.gradle.kts
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation("androidx.room:room-runtime:3.0.0-alpha01")
implementation("androidx.room:room-ktx:3.0.0-alpha01")
ksp("androidx.room:room-compiler:3.0.0-alpha01")
}
// All DAO methods must be suspend or Flow — no blocking operations
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): User?
@Query("SELECT * FROM users")
fun observeAllUsers(): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Transaction
suspend fun updateUserWithHistory(user: User, history: UserHistory) {
insertUser(user)
insertHistory(history)
}
}
Room 3.0 — KMP Targets:
// shared/build.gradle.kts — Room 3.0 KMP configuration
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
macosArm64()
sourceSets {
commonMain.dependencies {
implementation("androidx.room:room-runtime:3.0.0-alpha01")
}
androidMain.dependencies {
implementation("androidx.sqlite:sqlite-android:2.5.0")
}
iosMain.dependencies {
implementation("androidx.sqlite:sqlite-bundled:2.5.0")
}
}
}
Hilt 2.56: Adds support for @HiltViewModel with Lifecycle 2.9’s new scoped ViewModel APIs. ViewModels scoped to Compose subtrees can now receive Hilt injection.
Navigation 3.0.0-alpha07: Adds NavHost support for predictiveBack — the gesture-based back navigation animation introduced in Android 14 now works with Navigation 3’s back stack model.
Lottie Compose 6.6.0: Adds LottieAnimation support for Compose Multiplatform (Android + iOS + Desktop). The animation files are shared; rendering uses platform-native compositing.
Room 3.0 — What Breaks
Key breaking changes to be aware of:
// ❌ Room 3.0 — these patterns no longer compile
// Blocking DAO method — not allowed
@Query("SELECT * FROM users")
fun getAllUsersBlocking(): List<User> // Compile error: must be suspend or Flow
// LiveData return — removed
@Query("SELECT * FROM users")
fun observeUsers(): LiveData<List<User>> // Compile error: LiveData not supported
// Java annotation processing — no longer works
// kapt("androidx.room:room-compiler:3.0.0-alpha01") // Use ksp() instead
// ✅ Room 3.0 — correct patterns
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<User> // OK
@Query("SELECT * FROM users")
fun observeUsers(): Flow<List<User>> // OK
📱 Android Platform Updates
Android 17 — Platform Stability Approaching
With Beta 2 out, Google is targeting API lock for Beta 3 in late March. Now is the time to:
- Update
compileSdkto 37 in a test branch - Run your full test suite
- Verify background audio playback behavior (new restrictions in Android 17)
- Check any
Networkpermission usage forACCESS_LOCAL_NETWORKimpacts
Wear Compose Remote — Cross-Device Compose
The new wear-compose-remote artifact (from the Jetpack March wave) allows rendering Compose UI on a paired Wear OS watch from the phone companion app. The watch acts as a display surface — the Compose tree runs on the phone:
// Phone app — renders UI on the paired watch
val wearRemote = WearComposeRemote.getInstance(context)
wearRemote.setContent {
// This composable renders on the watch
WearTheme {
TimeDisplay(currentTime = remember { mutableStateOf(LocalTime.now()) })
}
}
🌟 Community Spotlight
Tracey — Flight Recorder for Compose: Open-source KMP library that records gestures, screen views, breadcrumbs, and crashes for later replay. Think Bugsnag’s session replay but built entirely on Compose’s accessibility tree and gesture tracking. Works on Android, iOS, and Desktop. The replay viewer runs in the browser.
Compose LazyLayout Adaptive Grid: A library providing a LazyAdaptiveGrid composable that automatically adjusts column count based on available width. Less opinionated than LazyVerticalGrid(GridCells.Adaptive()) — handles non-uniform item sizes cleanly.
💡 Quick Tip of the Week
Use @Transaction in Room for operations that span multiple DAO calls:
@Dao
interface OrderDao {
// ❌ Not atomic — if insertItems fails, the order header was still inserted
suspend fun createOrder(order: Order, items: List<OrderItem>) {
insertOrder(order) // Call 1
insertItems(items) // Call 2 — could fail
}
// ✅ Wrapped in @Transaction — all-or-nothing
@Transaction
suspend fun createOrderAtomic(order: Order, items: List<OrderItem>) {
insertOrder(order)
insertItems(items)
}
@Insert suspend fun insertOrder(order: Order)
@Insert suspend fun insertItems(items: List<OrderItem>)
}
@Transaction wraps the entire function body in a database transaction. If any operation throws, the entire transaction is rolled back. This is one of the most commonly missed Room patterns in real apps.
🧠 Did You Know?
Room’s @Relation annotation fetches related entities in a SEPARATE query, not a SQL JOIN. This means a @Transaction is required to get a consistent snapshot when fetching parent + related entities:
data class UserWithOrders(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "userId"
)
val orders: List<Order>
)
@Dao
interface UserDao {
// ❌ Without @Transaction: user and orders could be from different DB states
@Query("SELECT * FROM users")
fun getUsersWithOrders(): Flow<List<UserWithOrders>>
// ✅ With @Transaction: consistent snapshot guaranteed
@Transaction
@Query("SELECT * FROM users")
fun getUsersWithOrders(): Flow<List<UserWithOrders>>
}
Room will actually warn you (not error) if you use @Relation without @Transaction. The warning is easy to miss, but the consequence is real: in high-write apps, you can read a user that no longer has the orders that were present a millisecond earlier.
❓ Weekly Quiz
Q1: What are the two breaking changes in Room 3.0 that affect the most existing codebases?
A: (1) All DAO methods must be suspend or return Flow — blocking Room operations are removed. (2) LiveData as a return type from DAO methods is no longer supported.
Q2: Why does Room’s @Relation require @Transaction for correct behavior?
A: @Relation fetches related entities in a separate query from the parent entity query. Without @Transaction, the two queries could read from different database states if a write occurs between them. @Transaction ensures both queries see a consistent snapshot.
Q3: What Kotlin tooling change does Room 3.0 require, compared to Room 2.x?
A: Room 3.0 requires KSP (Kotlin Symbol Processing) instead of KAPT. The ksp() dependency declaration replaces kapt() in build.gradle.kts. Java annotation processing is no longer supported.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Android 17 Beta 2: EyeDropper, Contacts Picker, Cross-Device Handoff”: Beta 2 introduces the full complement of new Android 17 user-facing APIs. The standout: EyeDropper API for system-level color picking (no screen capture permission required), ACTION_PICK_CONTACTS for session-based contact data access, and cross-device app handoff via CompanionDeviceManager. Platform stability (API lock) is targeted for late March with Beta 3.
JetBrains — “KotlinConf ‘26 Full Agenda Published”: The KotlinConf ‘26 schedule is live. June 3 is workshop day, June 4 is the main conference in Copenhagen. Keynote topics include Kotlin 2.3 improvements, Kotlin Multiplatform production stories, and the future of Kotlin/Wasm. Featured speakers: Márton Braun (JetBrains), Arkadii Ivanov (xAI), and Roman Elizarov.
Dave Leeds — “Flows for Sequence Developers”: A gentle introduction to Kotlin Flows using sequences as the mental model. Sequences are synchronous and lazy; Flows are asynchronous and lazy. The transformation APIs (map, filter, flatMap) are nearly identical. This framing removes a lot of the intimidation factor for developers coming from Sequence-heavy codebases.
“Android 17 Beta 2 — What Every Developer Needs to Test”: A community checklist for Beta 2 compatibility testing. Priority items: local network access blocks (requires ACCESS_LOCAL_NETWORK), audio focus behavior changes, and the new resizability requirements for apps targeting API 37.
“Dependency Guard by Dropbox — Preventing Accidental Dependency Bumps in CI”: An updated tutorial on using Dropbox’s Dependency Guard plugin to prevent accidental transitive dependency changes from slipping into production. Particularly useful for apps with strict security or compliance requirements.
“Kotlin Flows in Practice: Cold vs. Hot, StateFlow vs. SharedFlow”: A practical guide to choosing between Flow, StateFlow, and SharedFlow. The decision tree: use Flow for one-shot data operations; StateFlow for UI state that should replay the last value; SharedFlow for events that should be received by multiple collectors but not replayed.
🛠️ Releases & Version Updates
Android 17 Beta 2: Available on Pixel 6+ devices and emulator images. Final API surface is expected to lock with Beta 3 at platform stability. Apps can now target compileSdk = 37 for beta testing.
Compose BOM 2026.03.00-alpha01: Includes Compose 1.9.0-alpha01 with the new BasicHtmlText composable for rendering basic HTML (bold, italic, links) without a full WebView.
Ktor 3.1.1: Patches a memory leak in HttpClient when the response body is discarded without reading. Affects long-running apps that make many requests to endpoints that return large bodies (file downloads, large JSON responses).
SQLDelight 2.1.0: Adds incremental compilation support for KSP. Previously, any schema change caused a full regeneration of all generated Kotlin types. With 2.1.0, only affected tables are regenerated, cutting incremental build times by 50–70% for large schemas.
Gradle 9.3: Compatibility verified with Kotlin 2.3.x (coming next week). Key change: buildSrc no longer inherits settings from the root project’s settings.gradle.kts. If you use buildSrc with shared plugin declarations, migration is needed.
Android 17 Beta 2 — EyeDropper API
The new system-level color picker requires no permissions:
@Composable
fun ColorPickerButton(onColorSelected: (Color) -> Unit) {
val context = LocalContext.current
val eyeDropperLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val colorInt = result.data?.getIntExtra(Intent.EXTRA_COLOR, Color.Black.toArgb())
colorInt?.let { onColorSelected(Color(it)) }
}
}
Button(onClick = {
val intent = Intent(Intent.ACTION_OPEN_EYE_DROPPER)
eyeDropperLauncher.launch(intent)
}) {
Icon(Icons.Default.Colorize, contentDescription = "Pick color")
Spacer(Modifier.width(8.dp))
Text("Pick Color from Screen")
}
}
Kotlin Flows — Cold vs. Hot
// Cold Flow — producer code runs for each collector independently
val coldFlow: Flow<Int> = flow {
println("Starting emission") // runs ONCE per collector
(1..5).forEach { emit(it) }
}
// Hot StateFlow — single producer, all collectors see same stream
val hotFlow: StateFlow<Int> = MutableStateFlow(0)
// Cold flows: good for one-shot data loading
// Hot flows: good for shared UI state, event buses
// Converting cold to hot — use shareIn
val sharedColdFlow = coldFlow
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
replay = 1
)
📱 Android Platform Updates
Android 17 — Bubbles Windowing Mode
The Bubbles API (distinct from messaging bubbles) allows apps to appear as floating bubbles that users can drag, minimize, and expand. This is a windowing mode, similar to Picture-in-Picture but more general:
// Opt into bubbles support in your manifest
// <activity
// android:name=".BubbleActivity"
// android:allowEmbedded="true"
// android:documentLaunchMode="always"
// android:resizeableActivity="true" />
// Create a bubble notification
val bubbleIntent = PendingIntent.getActivity(
context, 0,
Intent(context, BubbleActivity::class.java),
PendingIntent.FLAG_MUTABLE
)
val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(
bubbleIntent,
IconCompat.createWithAdaptiveBitmap(bubbleBitmap)
)
.setDesiredHeight(600)
.setSuppressNotification(true)
.setAutoExpandBubble(false)
.build()
Advanced UWB Ranging — Indoor Navigation
Android 17 adds UWB DL-TDOA support for privacy-preserving indoor navigation. The key: the device can navigate via UWB anchors without the anchors being able to track the device’s identity:
// UWB ranging with Android 17's DL-TDOA support
val uwbManager = context.getSystemService(UwbManager::class.java)
val controllerSession = uwbManager.controllerSessionScope()
// Build ranging parameters
val rangingParams = RangingParameters.Builder()
.setUwbConfigType(RangingParameters.UWB_CONFIG_ID_3)
.setSessionId(Random.nextInt())
.setRangingUpdateRate(RangingParameters.RANGING_UPDATE_RATE_FREQUENT)
.build()
🎤 Conferences & Videos
KotlinConf ‘26 — June 4, Copenhagen: The full agenda is live. Workshop day is June 3. Grab the community ticket (179€ + VAT) before they sell out. The festival vibe format from previous years continues — outdoor stages, live demos, and late evening sessions.
“Flows in Android Development” (Android Developers YouTube): Dave Leeds’ talk translated into a YouTube walkthrough, covering the full Flow API surface with real Android examples: database observation, network polling, and combining multiple data sources with combine.
🧰 Tool of the Week
Dependency Guard (Dropbox): Catches accidental dependency changes in CI. Zero configuration to start:
// build.gradle.kts (root)
plugins {
id("com.dropbox.dependency-guard") version "0.5.0"
}
dependencyGuard {
configuration("releaseRuntimeClasspath") {
tree = true // include transitive deps in baseline
modules = true
}
}
# Generate baseline — commit this file
./gradlew dependencyGuard
# Check for changes in CI — fails if any dependency changed
./gradlew dependencyGuardCheck
💡 Quick Tip of the Week
Use flowOf and emptyFlow for static or absent data in tests and previews:
// In tests — no need for a real repository
class UserViewModelTest {
@Test
fun `shows user name when loaded`() = runTest {
val fakeRepo = object : UserRepository {
override fun observeUser(): Flow<User?> =
flowOf(User(id = "1", name = "Mukul"))
}
val vm = UserViewModel(fakeRepo)
vm.uiState.test {
val state = awaitItem()
assertEquals("Mukul", (state as UserState.Success).user.name)
}
}
}
// In Compose Previews — use empty flow for no-data states
@Preview
@Composable
fun UserCardPreview() {
val previewVm = object : UserViewModel(
object : UserRepository {
override fun observeUser() = emptyFlow<User?>()
}
) {}
UserCard(previewVm)
}
🧠 Did You Know?
SharedFlow with replay = 0 is a true event bus — but it has a subtle flaw that replay = 1 doesn’t. If no collector is active when an event is emitted, the event is lost permanently. This is intentional for UI events (you don’t want to re-process a button click after returning from the back stack), but it catches developers off guard:
class EventViewModel : ViewModel() {
// replay = 0: events not collected are LOST
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()
// replay = 1: last event replayed to new collector
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state = _state.asStateFlow()
fun triggerNavigation(destination: String) {
viewModelScope.launch {
_events.emit(UiEvent.Navigate(destination)) // lost if no collector active
}
}
}
// Collect events only while the lifecycle is STARTED
// This prevents collecting stale navigation events from before the screen was visible
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
when (event) {
is UiEvent.Navigate -> navController.navigate(event.destination)
}
}
}
The modern recommendation: use Channel with Channel.BUFFERED for one-time events, and SharedFlow(replay=0) for fire-and-forget broadcasts to many collectors simultaneously.
❓ Weekly Quiz
Q1: What is the Android 17 EyeDropper API used for, and what permission is required?
A: The EyeDropper API allows apps to request a color from any pixel on the display. No special permission is required — the system handles the screen capture internally and returns only the selected color to the requesting app.
Q2: What is the key difference between SharedFlow(replay=0) and StateFlow for event handling?
A: SharedFlow(replay=0) is a “hot” flow where events emitted when no collector is active are lost. StateFlow always has a value and replays the last value to new collectors. Use SharedFlow(replay=0) for UI events (navigate, show snackbar), StateFlow for persistent UI state.
Q3: What changed in SQLDelight 2.1.0 that improves incremental build performance?
A: SQLDelight 2.1.0 adds incremental compilation support for KSP — only the Kotlin types for modified tables are regenerated on schema changes, instead of regenerating all types. This typically cuts incremental build times by 50–70% for large database schemas.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Jesse Wilson — “Oppose Google’s Developer Verification Mandate”: Jesse raises the alarm about Google’s proposed 2026 developer verification requirement — a paid, identity-verified registration system that would be required to distribute apps outside the Play Store. His argument: this is anti-competitive, would prevent developers in many regions from building Android apps, and undermines Android’s openness as a platform. The post generated significant community debate.
Zac Sweers — “The Real DI Tradeoff: Compile-Time vs. Runtime”: Zac reframes the dependency injection debate not as “Dagger vs. Koin vs. Hilt” but as a fundamental question: compile-time graph construction versus runtime service lookup. Compile-time (Dagger/Hilt/Metro) catches errors at build time, produces optimized code, but has higher build cost. Runtime (Koin, Kodein) trades correctness guarantees for faster incremental builds and simpler setup. Neither is universally better — but the tradeoffs are often misunderstood.
Andrius Semionovas & Heorhii Popov — “Migrating a Large Codebase from Dagger/Anvil to Metro”: A case study of migrating an enterprise Android app from Dagger + Anvil to Metro (JetBrains’ newer DI framework for Kotlin). The result: K2 compiler compatibility unlocked, incremental build times improved by ~40%, and the codebase dropped significant amounts of boilerplate. The migration wasn’t painless — particularly around component scoping and multi-module setups.
“Keep Android Open: What the 2026 Verification Mandate Would Mean”: A detailed breakdown of the technical and geopolitical implications of mandatory developer identity verification for Android app distribution. Worth reading regardless of your stance on the policy question.
“Five Common Compose Performance Anti-Patterns”: Philipp Lackner identifies five patterns that reliably cause performance problems in Compose apps: (1) reading mutable state inside measure phases, (2) using State in draw callbacks, (3) unstable lambda captures in LazyColumn, (4) excessive use of derivedStateOf with no key logic, and (5) painting overdraw with opaque backgrounds on transparent containers.
“Building a Modular Android Debug Toolkit with Compose”: A walkthrough of building a production-safe debugging overlay using Compose, with a clean architecture that compiles to a no-op in release builds. The trick: an expect/actual declaration where the debug implementation shows the overlay and the release implementation is empty.
🛠️ Releases & Version Updates
Kotlin 2.2.0-Beta2: JetBrains continues the Kotlin 2.2 beta cycle. This build improves K2 compiler diagnostics — error messages now include the full resolution path when an overload can’t be resolved, making “None of the following candidates is applicable” errors dramatically more useful.
Metro 1.0.0-beta01 (JetBrains DI): Metro is JetBrains’ Kotlin-first DI framework — think Dagger with K2 support from the ground up. The beta adds Anvil-compatible component scoping syntax:
// Metro — Kotlin-first DI with K2 support
@ContributesTo(AppScope::class)
@Module
interface UserModule {
@Binds
fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}
@ContributesBinding(AppScope::class)
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val db: UserDatabase
) : UserRepository
// Component — generated at compile time, no reflection
@Component(scope = AppScope::class)
interface AppComponent {
val userRepository: UserRepository
val userViewModel: UserViewModel
}
WorkManager 2.10.1: Fixes a regression where PeriodicWorkRequest could be delayed indefinitely on devices with aggressive Doze mode when no constraint network was required.
AndroidX DataStore 1.1.2: Improves migration from SharedPreferences to DataStore with a new SharedPreferencesMigration that handles Long values correctly (a regression introduced in 1.1.0).
Metro vs. Dagger — The Build Time Story
The key benchmark from the Andrius/Heorhii migration:
// Dagger/Anvil (K1 compiler):
// Clean build: ~4.2 min
// Incremental build (single file change): ~1.8 min
// K2 support: partial (via kapt compatibility mode)
// Metro (K2 compiler):
// Clean build: ~3.8 min (9% faster)
// Incremental build (single file change): ~1.1 min (39% faster)
// K2 support: native
The incremental build improvement is where Metro shines — it leverages the K2 compiler’s better incremental compilation support.
📱 Android Platform Updates
Android 17 — SMS OTP Protection Hardening
One behavior change that will affect authentication flows: Android 17 extends OTP SMS protection. For apps targeting API 37, SMS messages containing OTP codes are delayed by 3 hours for most apps, preventing OTP hijacking:
// The RIGHT way to handle SMS OTPs — works on all Android versions
// Step 1: Generate app-specific hash
// Step 2: Include hash in SMS message from your server
// Step 3: Use SMS Retriever API — exempt from the 3-hour delay
class SmsRetrieverReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as? Status
if (status?.statusCode == CommonStatusCodes.SUCCESS) {
val message = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE) as? String
val otp = extractOtpFromMessage(message)
onOtpReceived(otp)
}
}
}
}
// Register and start retriever
val client = SmsRetriever.getClient(context)
val task = client.startSmsRetriever()
Apps that use READ_SMS for OTP extraction should migrate to SMS Retriever or SMS User Consent APIs before Android 17 ships.
Background Audio Changes (Android 17 Preview)
Android 17 will restrict background audio interactions. Apps in the background that attempt to request audio focus or adjust volume will require FOREGROUND_SERVICE_MEDIA_PLAYBACK or active media session ownership. Start testing your audio playback flows now.
⚠️ Deprecation & Migration Alerts
kapt Soft Deprecation in AGP 9.x: AGP 9.0 (expected mid-2026) will require explicit opt-in to use KAPT. All annotation processors should be migrated to KSP. Key libraries with KSP support: Room (2.6+), Hilt (2.49+), Glide (4.16+), Moshi (1.15+). Libraries that don’t yet support KSP need kaptToKspMapping workaround in AGP 8.x.
SharedPreferences — Performance Deprecation Warning: SharedPreferences continues to work but the documentation now explicitly states that DataStore is the recommended alternative. The apply() vs commit() confusion and main-thread blocking risks are the primary motivations.
🌟 Community Spotlight
“Android Verification Push Back Movement”: The Android developer community organized quickly around Jesse Wilson’s post, with hundreds of developers filing feedback on the Android issue tracker. As of this writing, Google has not responded publicly, but the volume of developer concern is significant.
💡 Quick Tip of the Week
Use derivedStateOf to compute derived state only when dependencies change — not on every recomposition:
// ❌ Recomputes on every recomposition, even if items didn't change
@Composable
fun ShoppingCart(items: List<CartItem>) {
val total = items.sumOf { it.price * it.quantity } // recalculates every time
Text("Total: $$total")
}
// ✅ Only recomputes when items reference changes
@Composable
fun ShoppingCart(items: List<CartItem>) {
val total by remember(items) {
derivedStateOf { items.sumOf { it.price * it.quantity } }
}
Text("Total: $$total")
}
derivedStateOf is particularly valuable when the derived computation is expensive (sorting, filtering, aggregation) and the source list changes frequently via mutableStateListOf.
🧠 Did You Know?
Kotlin’s inline functions have a compile-time cost that most developers underestimate. Every call site of an inline function causes the function’s bytecode to be copied to that call site. This is great for lambdas (eliminates object allocation) but terrible for large functions called in many places:
// ❌ Inline a large function called in 100 places = 100x bytecode copies
inline fun <T> processWithRetry(action: () -> T): T {
var attempts = 0
while (attempts < 3) {
try {
return action()
} catch (e: IOException) {
attempts++
delay(1000L * attempts) // Bytecode for all of this copied to each call site
}
}
throw IOException("Failed after 3 attempts")
}
// ✅ Only inline the lambda, keep the rest as a regular function
fun <T> processWithRetry(action: suspend () -> T): T {
// Regular function body — called at one location
return retryLogic(action)
}
// Or make it suspend and non-inline — coroutines handle the lambda efficiently
suspend fun <T> retryWithBackoff(action: suspend () -> T): T { ... }
The rule of thumb: inline when your function takes a lambda parameter and the function body is small (5–15 lines). For larger functions or functions called from many places, inline is often a net negative.
❓ Weekly Quiz
Q1: What is the primary build-time advantage of Metro over Dagger when both are used with the K2 compiler?
A: Metro was designed for K2 from the ground up, enabling better incremental compilation support. The case study in this issue showed ~39% faster incremental builds compared to Dagger/Anvil with K2 compatibility mode.
Q2: What happens to SMS OTP messages for apps targeting Android 17 that don’t use SMS Retriever format?
A: They are delayed by 3 hours before becoming accessible via the SMS provider or SMS_RECEIVED_ACTION. Apps should migrate to SMS Retriever or SMS User Consent APIs to maintain immediate OTP access.
Q3: When is derivedStateOf preferable to a plain remember(key) block in Compose?
A: derivedStateOf is preferable when the derived value is read from multiple state objects that update at different rates. derivedStateOf only re-executes when the actual result of the computation would change, whereas remember(key) re-executes whenever any listed key changes reference.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Google — “Android 17 Beta 1: Adaptability, Media, and Connectivity”: The first beta of Android 17 drops with a focus on large screen adaptability, improved companion device tooling, and media improvements. The headline API additions: cross-device app handoff, advanced UWB ranging for indoor navigation, and the new ACCESS_LOCAL_NETWORK runtime permission. Google confirmed the annual release cadence continues, with platform stability targeted for late March.
Costa Fotiadis — “Replacing ViewModel with Compose’s Retain API”: An exploration of rememberRetained() (from Slack’s Circuit library) and the new built-in retainedStateHolder() in Compose — APIs that let you retain state through recomposition and configuration changes without the overhead of a full ViewModel. The key difference: retained objects are scoped to the composition tree, not the Activity lifecycle.
Arman Chatikyan — “Remote Compose: Server-Driven UI Without WebViews”: Google’s Remote Compose enables defining and rendering Android UI from a server using a serialized Compose description format. No WebViews, no custom JSON schemas — the server sends a composable layout description, and the client renders it using real Compose. The article shows the current API limitations and where it’s currently viable.
“Jetpack Stack on KMP: A Complete Migration Guide”: Thomas Künneth expanded his earlier article into a full guide with migration checklists for ViewModel → circuit/molecule, Room → SQLDelight, DataStore → multiplatform-settings, and Navigation → decompose or circuit. The guide includes an honest assessment of KMP readiness for production apps.
“Android 17 Beta 1 Feature Deep Dive”: A community deep-dive into the Android 17 Beta 1 release notes. Highlights: the Bubbles windowing API (for app bubbles, not messaging), the ACTION_PICK_CONTACTS privacy-preserving contacts picker, and the new time zone offset broadcast ACTION_TIMEZONE_OFFSET_CHANGED.
“Compose retain vs ViewModel: When to Use Each”: A pragmatic comparison. TL;DR: use ViewModel for data that outlives configuration changes in non-Compose architectures or when you need SavedStateHandle / dependency injection. Use retain APIs for Compose-scoped state that should survive recomposition but doesn’t need to persist across process death.
🛠️ Releases & Version Updates
Android 17 Beta 1: Available on Pixel 6+ devices and the Android Emulator. This beta introduces new APIs but does not yet lock the final API surface — use it for early compatibility testing, not final targetSdk bumps.
Compose BOM 2026.02.00: Stable release with Compose UI 1.8.0, Material3 1.4.0 stable, and Compose Animation 1.8.0. The AnimatedContent API receives a new SizeTransform that handles layout bounds changes during transitions.
// Compose 1.8.0 — AnimatedContent with SizeTransform
AnimatedContent(
targetState = isExpanded,
transitionSpec = {
fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { expanded ->
if (expanded) ExpandedContent() else CollapsedContent()
}
Circuit 0.25.0 (Slack): The Compose-first architecture library adds rememberRetained() support for arbitrary objects and improves the Presenter lifecycle integration with Activity result handling.
Accompanist 0.37.0 — Final Release: This is explicitly the last Accompanist release. All remaining useful APIs (Pager, FlowLayout) have been migrated to Compose Foundation. The library will not receive further updates.
Android 17 Beta 1 — Cross-Device Handoff API
The Handoff API lets your app specify state that can be resumed on another nearby Android device:
class MainActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
// Opt into cross-device handoff
setHandoffEnabled(true)
}
// Provide the state to hand off — called by the system before suggesting handoff
override fun onProvideHandoffUserActivityState(extras: Bundle) {
extras.putString("current_screen", currentScreen)
extras.putString("scroll_position", scrollPosition.toString())
}
// Receive handoff from another device
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.hasExtra("handoff_extras")) {
val screen = intent.getStringExtra("current_screen")
navigateTo(screen)
}
}
}
📱 Android Platform Updates
Android 17 — New Privacy APIs
Privacy-Preserving Contacts Picker: Instead of requesting READ_CONTACTS, apps can now use ACTION_PICK_CONTACTS with explicit data field requests. The user grants temporary session-based access only to the fields they choose to share:
val contactPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val contactUri = result.data?.data ?: return@rememberLauncherForActivityResult
processContactData(contactUri)
}
}
val intent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
putStringArrayListExtra(
EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
arrayListOf(Email.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE)
)
putExtra(EXTRA_ALLOW_MULTIPLE, true)
}
contactPicker.launch(intent)
Local Network Access Permission: Apps targeting Android 17 must declare ACCESS_LOCAL_NETWORK to discover devices on the local network (smart home, casting receivers). Apps that have previously been granted NEARBY_DEVICES group permissions won’t be prompted again.
🎤 Conferences & Videos
Android Developers YouTube — “What’s New in Android 17 Beta 1”: The official walkthrough of Beta 1 APIs. The cross-device handoff demo is particularly compelling — you can see a user switching from phone to tablet and picking up exactly where they left off.
“Remote Compose at Google”: A talk from Android engineers explaining the motivation behind Remote Compose. The primary use case: Wear OS watch faces served from a phone without requiring the watch app to be updated. Secondary: server-driven home screen widgets.
🌟 Community Spotlight
Circuit 0.25.0 rememberRetained(): Slack’s open-source architecture library continues to be ahead of the curve. rememberRetained() is the prototype for what will eventually land in core Compose — state that survives configuration changes without ViewModel overhead. Worth following the Circuit project closely.
💡 Quick Tip of the Week
Use rememberUpdatedState to safely capture the latest lambda in long-lived effects:
// ❌ Bug: onTimeout captures the initial lambda, never updates
@Composable
fun TimeoutEffect(onTimeout: () -> Unit) {
LaunchedEffect(Unit) {
delay(5000)
onTimeout() // Always calls the FIRST onTimeout — even if it changed
}
}
// ✅ Correct: rememberUpdatedState always reads the latest value
@Composable
fun TimeoutEffect(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(5000)
currentOnTimeout() // Always calls the CURRENT onTimeout
}
}
This pattern is essential in LaunchedEffect(Unit) — effects that run once but need to call the latest version of a callback. The effect doesn’t restart, but the lambda stays fresh.
🧠 Did You Know?
Accompanist was born because Google’s Compose API surface was still evolving too quickly for AndroidX to ship stable implementations. Libraries like SwipeRefresh, Pager, and SystemUIController in Accompanist were explicit bridge APIs — they existed to fill gaps that Google planned to fill in the official libraries. Now that HorizontalPager is in Compose Foundation and enableEdgeToEdge() handles system bar coloring, Accompanist has fulfilled its purpose. The final 0.37.0 release marks the end of one of the most successful “community bridge” libraries in the Android ecosystem.
// Old Accompanist pattern (now removed)
// val systemUiController = rememberSystemUiController()
// systemUiController.setStatusBarColor(MaterialTheme.colorScheme.primary)
// New way — single call in onCreate, zero compose dependency
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // handles everything
super.onCreate(savedInstanceState)
setContent { AppTheme { AppContent() } }
}
}
❓ Weekly Quiz
Q1: What is the key difference between the ACCESS_LOCAL_NETWORK permission introduced in Android 17 and older network access?
A: Previously, apps could freely access local network devices (LAN) without any permission. Android 17 requires declaring ACCESS_LOCAL_NETWORK at runtime to discover or connect to LAN devices. Apps that already hold other NEARBY_DEVICES group permissions won’t be re-prompted.
Q2: What specific advantage does rememberRetained() have over ViewModel in a Compose-first architecture?
A: rememberRetained() is scoped to the composition tree, not to the Activity/Fragment lifecycle. This means you can have multiple independent retained state holders at different levels of the UI hierarchy without requiring a ViewModelStoreOwner for each one.
Q3: What did Accompanist’s final 0.37.0 release signal about the state of Compose’s official API surface?
A: That Compose’s core APIs are now mature enough to handle everything Accompanist was bridging. All key Accompanist components (Pager, FlowLayout, edge-to-edge) have landed in official AndroidX libraries, making the bridge library unnecessary.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Arnaud Giuliani — “Koin Compiler Plugin: Auto-wiring Kotlin DI”: The official announcement of the Koin Compiler Plugin for Koin 4.1. The plugin works as a Kotlin Compiler Plugin (not just a Gradle plugin) — it scans your annotated classes at compile time, builds the dependency graph, and generates the Koin module declarations. The result: zero runtime reflection, earlier error detection, and ~30% fewer lines of DI boilerplate.
Marcello Galhardo — “Scoping ViewModels to Compose UI Hierarchy with New Lifecycle APIs”: New Lifecycle ViewModel APIs arriving in the 2.9.x series allow scoping ViewModels to arbitrary Compose UI subtrees, not just navigation destinations or Activities. This enables component-level state survival without global singletons or nav graph hacks. The article shows the full API surface with before/after comparisons.
“Walk Through an LLM Pipeline: Tokenization to Inference”: Not Android-specific, but essential reading if you’re integrating on-device AI. The article walks through tokenization, embeddings, attention, and inference in plain language with diagrams. Understanding this pipeline lets you make informed decisions about model size, quantization, and caching strategies for Android.
Stevdza-San — “Koin Compiler Plugin in Practice”: A practical walkthrough building a feature module with the Koin Compiler Plugin from scratch. Covers auto-detect constructor injection, the @Module and @ComponentScan annotations, and how the plugin integrates with existing manually-declared Koin modules.
“Retrofit vs OkHttp: What Android Developers Actually Need to Know”: Amit Shekhar’s comparative guide to Retrofit and OkHttp, explained from the perspective of what each one is actually for. The short answer: OkHttp is the HTTP client, Retrofit is the type-safe API layer on top. You almost always want both, and the article explains when and why to customize each independently.
Peter Friese — “Porting a To-Do App from iOS to Android Using AI Agents”: Peter and Marina Coelho used AI coding agents (Antigravity and Stitch) to port their SwiftUI “Make It So” app to Compose. The honest assessment: AI handles boilerplate and API mapping well but struggles with architectural decisions and Compose-specific idioms like remember and state hoisting.
🛠️ Releases & Version Updates
Koin 4.1.0-alpha01 with Compiler Plugin: The alpha release of Koin 4.1 with the new compiler plugin. Here’s the full setup:
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.1.10-1.0.30"
}
dependencies {
implementation("io.insert-koin:koin-android:4.1.0-alpha01")
implementation("io.insert-koin:koin-androidx-compose:4.1.0-alpha01")
ksp("io.insert-koin:koin-ksp-compiler:4.1.0-alpha01")
}
// Annotate your classes — the compiler generates the module
@Single
class UserRepositoryImpl(
private val api: UserApi,
private val prefs: UserPreferences
) : UserRepository
@KoinViewModel
class UserProfileViewModel(
private val repository: UserRepository,
savedStateHandle: SavedStateHandle
) : ViewModel()
// Your Application class — no manual module list needed
@KoinApplication
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@App)
// Compiler plugin generates UserModule.module automatically
}
}
}
Lifecycle 2.9.0-alpha03: The new ViewModelProvider.Factory extensions that allow ViewModel scoping to Compose subtrees:
// Scope a ViewModel to a specific part of the Compose tree
@Composable
fun ProductCard(productId: String) {
// This ViewModel lives as long as ProductCard is in the composition
// NOT tied to the nav back stack or Activity lifecycle
val viewModel = viewModel<ProductCardViewModel>(
key = productId,
factory = ProductCardViewModel.Factory(productId)
)
ProductContent(viewModel.uiState)
}
Compose 1.8.0-beta03: In this build, LazyColumn and LazyGrid get smarter content padding handling that respects window insets automatically when contentPadding is set via WindowInsets.asPaddingValues():
LazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues() +
PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(items) { item ->
ItemCard(item)
}
}
AGP 8.9.0-alpha05: Adds support for Gradle configuration caching for all BuildConfig generation tasks, shaving 10–30 seconds off clean builds in large projects.
Koin Compiler Plugin — What It Actually Generates
The compiler plugin generates a KoinApplication extension at compile time. You can inspect the generated code in your build directory:
// Generated by Koin KSP compiler — UserModule.kt (build/generated/ksp/...)
val userModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
}
fun Application.userModule() = koin.loadModules(listOf(userModule))
📱 Android Platform Updates
Android 17 Development Preview — What’s Coming
While the first official beta is still a few weeks away, the Android 17 developer documentation has been updated with the planned API surface. Key areas to watch:
- Bubbles API — app windowing mode, not messaging bubbles
- Cross-device handoff — resuming app state on another Android device
- Local Network Access permission — new
ACCESS_LOCAL_NETWORKruntime permission - EyeDropper API — system-level color picker
Background Work — WorkManager 2.10.0
The latest WorkManager stable release adds PeriodicWorkRequest support for ExpeditedWork:
// WorkManager 2.10.0 — expedited periodic work
val periodicSyncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic_sync",
ExistingPeriodicWorkPolicy.UPDATE,
periodicSyncRequest
)
🎤 Conferences & Videos
Android Developers YouTube — “Building with Gemini Nano on Android”: A hands-on video showing the ML Kit Prompt API for Gemini Nano integration. The demo builds a text summarization feature that runs entirely on-device on a Pixel 9 Pro. The cold-start latency is ~2 seconds; inference is ~50 tokens/second.
Fragmented Podcast — “AI Coding into Four Paradigms”: Kaushik Gopal and Iury Souza map AI-assisted coding into autocomplete, NL→code, test-driven generation, and agentic workflows. Worth listening to with your team to align on tooling expectations.
💡 Quick Tip of the Week
Use ViewModel.viewModelScope with SupervisorJob awareness for parallel operations:
class DashboardViewModel : ViewModel() {
fun loadDashboard() {
viewModelScope.launch {
// These run in parallel — if one fails, the other continues
// viewModelScope uses SupervisorJob internally
val userDeferred = async { userRepository.getUser() }
val feedDeferred = async { feedRepository.getFeed() }
val notificationsDeferred = async { notifRepository.getCount() }
// Collect results — errors propagate individually
val user = runCatching { userDeferred.await() }.getOrNull()
val feed = runCatching { feedDeferred.await() }.getOrElse { emptyList() }
val notifCount = runCatching { notificationsDeferred.await() }.getOrDefault(0)
_uiState.value = DashboardState(user, feed, notifCount)
}
}
}
viewModelScope uses a SupervisorJob — exceptions in one async block don’t cancel siblings. But you still need to handle them explicitly with runCatching or try/catch, otherwise they propagate to viewModelScope’s CoroutineExceptionHandler.
🧠 Did You Know?
The by viewModels() delegate in Compose creates the ViewModel in the nearest ViewModelStoreOwner — which might not be what you expect. In a Compose app using Navigation 3, each destination’s BackStackEntry is a ViewModelStoreOwner. So:
// In a NavHost destination composable:
@Composable
fun ProfileScreen() {
// This ViewModel is scoped to the NavBackStackEntry — destroyed when you pop
val vm: ProfileViewModel by viewModels()
}
// But if you call viewModel() outside a navigation context (e.g., in a top-level composable):
@Composable
fun ProfileScreen() {
// This ViewModel is scoped to the Activity — survives navigation
val vm: ProfileViewModel = viewModel()
}
The new Lifecycle 2.9.x APIs let you explicitly choose the ViewModelStoreOwner, which solves the “I want this ViewModel to live for this component only” problem cleanly.
❓ Weekly Quiz
Q1: What kind of errors does the Koin Compiler Plugin catch that the traditional Koin DSL cannot?
A: Missing dependency bindings. Without the compiler plugin, a missing single<>() or factory<>() declaration causes a NoBeanDefFoundException at runtime. The compiler plugin builds and validates the graph at build time.
Q2: In the new Lifecycle ViewModel scoping APIs, what is the default ViewModelStoreOwner when you call viewModel() inside a @Composable without specifying one?
A: The nearest LocalViewModelStoreOwner in the composition hierarchy — which for most Compose apps is the Activity. Inside Navigation 3, the nearest BackStackEntry takes priority.
Q3: What does SupervisorJob do inside viewModelScope, and why does it matter for parallel async operations?
A: SupervisorJob makes child coroutines independent — if one fails, the others continue running. This means parallel async operations in viewModelScope won’t cancel each other on failure, but you must still handle exceptions from each deferred.await() individually.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Marcin Moskała — “Compose Stability in 2026: Strong Skipping Mode Explained”: A thorough walkthrough of how Compose’s stability system works under Strong Skipping Mode (now enabled by default since Compose 1.7). The core insight: with Strong Skipping Mode, all lambdas are automatically memoized, and Compose distinguishes between reference equality (identity) and structural equality (value) when deciding whether to skip recomposition. This makes @Stable and @Immutable annotations less critical for common cases, but understanding when they still matter is the point of the article.
Arnaud Giuliani — “Koin Compiler Plugin: Compile-Time Safety for DI”: A preview of the upcoming Koin Compiler Plugin that brings constructor auto-detection and compile-time graph validation to Koin. Catch missing bindings at build time instead of at runtime. The plugin is still alpha, but the API looks solid and the potential for faster build feedback is significant.
“On-Device LLM Inference for Android Developers — A Practical Overview”: A practical overview of the current state of on-device inference on Android: MediaPipe’s LlmInference API, the Gemini Nano integration via ML Kit, and the LiteRT (formerly TFLite) runtime. The article benchmarks several models on Pixel 8 Pro and includes guidance on when on-device inference makes sense versus cloud API calls.
“Compose Navigation 3 Alpha: Building Real Apps”: A hands-on walkthrough building a real app with Navigation 3’s NavHost API. Key differences from Navigation 2: the back stack is now a pure List<Any> that you manage yourself, type-safe routes are first-class, and the deep-link integration has been redesigned around URI templates.
“R8 Full Mode — Enabling It Without Breaking Your App”: A guide to enabling R8 Full Mode (the new default in AGP 8.x) without the string-matching failures that plague apps using reflection. The fix is usually @Keep annotations and -keep ProGuard rules, but the article explains how to use apksig to identify what’s failing.
“Detekt Rules for Koin: Enforcing Best Practices at Scale”: Kirill Rozov’s new Detekt plugin adds rules for Koin that catch common anti-patterns: injecting too many dependencies into a single module, calling get() instead of constructor injection, and declaring modules in test code that shadow production modules.
🛠️ Releases & Version Updates
Room 2.7.0 Stable: The current stable Room release brings improved Kotlin Symbol Processing (KSP) support and coroutines-first DAO generation. If you haven’t migrated from KAPT to KSP for Room yet, now is a good time — build speed improvements are typically 20–40%:
// build.gradle.kts — switch from kapt to ksp for Room
plugins {
id("com.google.devtools.ksp") version "2.1.10-1.0.30"
}
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") // was: kapt(...)
}
Hilt 2.55: Works correctly with Room’s new KSP compiler. The @HiltViewModel annotation processor no longer requires KAPT if all your other annotation processors support KSP.
Coil 3.2.1: Patch for a memory leak in AsyncImage when used inside LazyColumn with animated content. The leak was caused by a disk cache reference not being released when the composable left composition. Upgrade if you use Coil with animated GIFs or WebP.
kotlinx.datetime 0.7.0: Adds TimeZone.currentSystemDefault() caching (avoids repeated JNI calls), DateTimePeriod arithmetic, and an Instant.toLocalDateTime() extension function. The API is still experimental but stable in practice:
// kotlinx.datetime 0.7.0 — clean date formatting
import kotlinx.datetime.*
val now = Clock.System.now()
val localNow = now.toLocalDateTime(TimeZone.currentSystemDefault())
// Format: "Feb 7, 2026"
val formatted = "${localNow.month.name.take(3).lowercase().replaceFirstChar { it.uppercase() }} " +
"${localNow.dayOfMonth}, ${localNow.year}"
Koin 4.1 — Compiler Plugin Preview
The compiler plugin turns Koin’s runtime DSL into compile-time graph construction:
// Before: Runtime declaration — errors caught at app startup
val userModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
viewModel { UserViewModel(get()) } // crashes if UserRepository not bound
}
// After: Compiler plugin — errors caught at build time
@Module
@ComponentScan("com.example.user")
class UserModule
// Constructor injection auto-detected by the compiler plugin
@Single
class UserRepositoryImpl(
private val api: UserApi,
private val database: AppDatabase
) : UserRepository
@KoinViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel()
📱 Android Platform Updates
On-Device AI — MediaPipe LLM Inference API
Google’s MediaPipe LlmInference API is the current recommended way to run Gemini Nano on-device. It requires Android 10+ and a device with sufficient RAM (typically 6+ GB for meaningful models):
// On-device LLM inference with MediaPipe
val inferenceOptions = LlmInference.LlmInferenceOptions.builder()
.setModelPath("/data/local/tmp/gemma-2b-it-gpu-int4.bin")
.setMaxTokens(1024)
.setTopK(40)
.setTemperature(0.8f)
.setRandomSeed(101)
.build()
val llmInference = LlmInference.createFromOptions(context, inferenceOptions)
// Streaming response
llmInference.generateResponseAsync(
prompt = "Summarize this Android release note in one sentence: $releaseNote"
) { partialResult, done ->
// partialResult arrives token by token
if (done) finalizeResponse(partialResult)
else appendToResponse(partialResult)
}
Freeform Window Mode Improvements (Android 16 QPR2)
Apps in freeform windowing mode on large screens can now opt into a minimum window size hint, preventing the system from resizing them below a usable threshold:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Declare minimum freeform window size
window.attributes = window.attributes.also {
it.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
}
WindowCompat.setDecorFitsSystemWindows(window, false)
}
🌟 Community Spotlight
“YARC — Compose Shorthand Generator”: The open-source tool that generates Compose layouts from shorthand syntax gained traction this week after being featured in the Android Weekly newsletter. If you build internal tooling or design systems, the shorthand approach is worth adapting.
Koin Detekt Rules (Kirill Rozov): Available on GitHub as detekt-koin-rules. The rules are composable with your existing Detekt configuration. The most useful rule: flagging Koin modules with more than 15 bindings (a sign of a module that should be split).
💡 Quick Tip of the Week
Avoid triggering recomposition from lambda captures — hoist state properly:
// ❌ Lambda captures mutable state — new lambda instance on every recomposition
@Composable
fun CounterButton(count: Int, onIncrement: () -> Unit) {
var localMultiplier by remember { mutableStateOf(1) }
Button(onClick = { repeat(localMultiplier) { onIncrement() } }) {
Text("Add $localMultiplier")
}
}
// ✅ Extract the callback computation to avoid capturing instability
@Composable
fun CounterButton(count: Int, onIncrement: (times: Int) -> Unit) {
var localMultiplier by remember { mutableStateOf(1) }
val onClickStable = remember(localMultiplier) {
{ onIncrement(localMultiplier) }
}
Button(onClick = onClickStable) {
Text("Add $localMultiplier")
}
}
With Strong Skipping Mode, this matters less for lambdas that don’t capture unstable state — but for lambda-heavy UI with many parameters, explicit remember(key) wrapping still buys you measurable recomposition reduction.
🧠 Did You Know?
Compose’s @Stable annotation doesn’t prevent recomposition — it tells the compiler to use structural equality for skip checks. An @Stable class will still recompose if any of its observed properties change; what changes is HOW the equality check is performed. Without @Stable, Compose uses reference equality (is it the same object in memory?). With @Stable, it calls equals(). This means:
// Without @Stable: recomposes every time UserProfile is recreated, even with same data
data class UserProfile(val name: String, val email: String)
// With @Stable: skips recomposition if equals() returns true
@Stable
data class UserProfile(val name: String, val email: String)
// In a ViewModel — emitting the same logical value triggers recomposition without @Stable
_uiState.update { it.copy() } // copy() creates new object — reference equality fails
// With @Stable data class, copy() with same values won't trigger recomposition
data class with @Stable is often the right choice for UI state objects. You get structural equality for free.
❓ Weekly Quiz
Q1: What does Compose Strong Skipping Mode change about how lambdas are handled?
A: All lambdas passed to composables are automatically memoized (wrapped in remember). This means a new lambda instance with the same captured values won’t trigger recomposition, reducing unnecessary UI updates.
Q2: What is the primary advantage of migrating Room’s annotation processing from KAPT to KSP?
A: KSP (Kotlin Symbol Processing) is faster — typically 20–40% reduction in incremental build times for annotation-heavy modules like Room. It also generates cleaner code and supports Kotlin Multiplatform natively.
Q3: In the Koin Compiler Plugin model, when are missing dependency bindings detected?
A: At build time (compile time). Without the compiler plugin, missing bindings in Koin cause NoBeanDefFoundException at runtime when the dependency is first requested.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
“Android’s Embedded Photo Picker — No More Permission Dialogs”: Google’s embedded photo picker is now the recommended way to let users select media from Android 14+ devices. Instead of requesting READ_MEDIA_IMAGES, you drop the picker in-place inside your own layout as a fragment, and the system grants scoped URI access. The article walks through the EmbeddedPhotoPicker API and its Activity Result contract wrapper.
JetBrains — “Qodana as CI-First Android Code Quality”: JetBrains frames Qodana as the CI-native counterpart to Android Lint — it runs Android Lint, Detekt, and its own inspections in a single pass, outputs SARIF, and integrates with GitHub Actions and TeamCity. The pitch: enforce code quality gates without adding per-developer tooling requirements.
sinasamaki — “ChromaDial: A Degree-Based Circular Dial in Compose”: A deep-dive into building a circular dial component with degree-based state, custom ranges, multi-turn visuals, and a snap callback that fires only when interaction ends (avoiding expensive recompositions on every degree change). The “finish callback” pattern is a great pattern for any gesture-heavy Compose UI.
“Kotlin Multiplatform for Android Developers: The Mental Shift”: A conversational piece aimed at Android developers approaching KMP for the first time. Key insight: expect/actual isn’t just for platform APIs — it’s the idiomatic way to provide different implementations of anything that doesn’t share a common abstraction across platforms.
“Flow vs. LiveData in 2026: The Final Word”: With Google’s @Discouraged annotation now on all LiveData APIs, this post makes the definitive migration argument. The migration path is StateFlow + collectAsStateWithLifecycle(), and the post provides before/after for every major LiveData use case including one-shot events.
“Compose Modifier Order: The Decorator Mental Model”: Marcin Moskała explains why modifier order matters by framing each modifier as a decorator. The insight: modifiers apply from outermost to innermost, which means padding().background() and background().padding() produce visually different results — and neither is “wrong,” they just mean different things.
🛠️ Releases & Version Updates
Compose Material3 1.4.0 Stable: The January stable release of Material3 brings SegmentedButton, SearchBar with suggestions support, and BottomAppBar with FAB docking. Dynamic color support is improved for Android 12+ devices:
@Composable
fun SortBar(
selected: SortOrder,
onSelected: (SortOrder) -> Unit
) {
SingleChoiceSegmentedButtonRow {
SortOrder.entries.forEachIndexed { index, order ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index, SortOrder.entries.size),
onClick = { onSelected(order) },
selected = order == selected,
label = { Text(order.label) }
)
}
}
}
Ktor 3.1.0: The multiplatform HTTP framework adds ContentNegotiation improvements and a new WebSocketSession.convertAndSend() extension for type-safe WebSocket message sending. The Darwin (Apple) engine now supports background URL sessions.
Kotlinx.coroutines 1.10.1: Patch release fixing a regression in StateFlow where collectors on Dispatchers.Main.immediate could miss the first emission. If you upgraded to 1.10 in January, update to 1.10.1 now.
Navigation 3.0.0-alpha05: The alpha continues to evolve. This build adds NavHost support for SharedTransitionLayout — you can now do shared element transitions between destinations without any custom code:
SharedTransitionLayout {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(
onItemClick = { id -> navController.navigate("detail/$id") },
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") { backStackEntry ->
DetailScreen(
id = backStackEntry.arguments!!.getString("id")!!,
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
Android Embedded Photo Picker API
The API surface is cleaner than it looks. Here’s the core integration:
// Register the launcher — works exactly like ActivityResult contracts
val photoPickerLauncher = registerForActivityResult(
PickVisualMedia()
) { uri ->
uri?.let { displayPhoto(it) }
}
// Single photo pick — no READ_MEDIA_IMAGES permission needed on Android 14+
fun launchPhotoPicker() {
photoPickerLauncher.launch(
PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
)
}
// Multiple photos — set a max selection limit
val multiPickerLauncher = registerForActivityResult(
PickMultipleVisualMedia(maxItems = 5)
) { uris ->
displaySelectedPhotos(uris)
}
📱 Android Platform Updates
Embedded Photo Picker — What It Means for Permission Architecture
The traditional permission flow for photo access has been fragmented: READ_MEDIA_IMAGES on API 33+, READ_EXTERNAL_STORAGE on older, and the visual media picker partial access on API 34+. The embedded picker consolidates all of this:
- API 30–33: Falls back to system photo picker (full-screen)
- API 34+: Renders inline within your layout using the
EmbeddedPhotoPickerfragment - No permission declaration needed for the inline picker path
This is the most significant permission architecture simplification since scoped storage.
ContentProvider Permission Hardening (Android 16+)
A less-publicized Android 16 change: apps can no longer grant URI permissions for FileProvider URIs to other apps unless the receiving app explicitly requests them. This affects apps that share files via Intent.FLAG_GRANT_READ_URI_PERMISSION and may require testing with existing file-sharing flows.
🧰 Tool of the Week
Qodana by JetBrains: If your team isn’t running automated code quality gates in CI, Qodana is worth trying. It bundles Android Lint, Detekt, and IntelliJ inspections into a single Docker image. The GitHub Action integration is straightforward:
# .github/workflows/code-quality.yml
- name: Run Qodana
uses: JetBrains/qodana-action@v2025.3
with:
pr-mode: false
env:
QODANA_TOKEN: $
The SARIF output integrates with GitHub’s code scanning dashboard, so issues appear inline on pull requests. The free tier covers most open-source use cases.
💡 Quick Tip of the Week
Use collectAsStateWithLifecycle() instead of collectAsState() in Compose:
// ❌ collectAsState() — keeps collecting even when the app is in the background
@Composable
fun UserProfile(viewModel: UserViewModel = hiltViewModel()) {
val user by viewModel.userState.collectAsState()
UserContent(user)
}
// ✅ collectAsStateWithLifecycle() — pauses collection when lifecycle is below STARTED
@Composable
fun UserProfile(viewModel: UserViewModel = hiltViewModel()) {
val user by viewModel.userState.collectAsStateWithLifecycle()
UserContent(user)
}
collectAsStateWithLifecycle() respects the Android lifecycle — it stops collecting when your app goes to the background, reducing unnecessary work. Add the lifecycle-runtime-compose dependency to get it:
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
🧠 Did You Know?
Compose remember without keys is a potential bug trap. If you use remember { expensiveComputation(someParam) } without listing someParam as a key, the computation only runs once — even if someParam changes on recomposition. The Compose compiler won’t warn you by default:
// 🐛 Bug: sortedItems never updates when items or sortOrder change
@Composable
fun ItemList(items: List<Item>, sortOrder: SortOrder) {
val sortedItems = remember { items.sortedBy { sortOrder.selector(it) } }
LazyColumn {
items(sortedItems) { ItemRow(it) }
}
}
// ✅ Correct: keys listed — recomputes when either changes
@Composable
fun ItemList(items: List<Item>, sortOrder: SortOrder) {
val sortedItems = remember(items, sortOrder) { items.sortedBy { sortOrder.selector(it) } }
LazyColumn {
items(sortedItems) { ItemRow(it) }
}
}
Starting with the Compose BOM 2026.01, the compiler will emit a warning when remember {} captures unstable state without declaring it as a key. Worth enabling @Suppress("REMEMBER_WITHOUT_KEYS") in the short term while you audit your code.
❓ Weekly Quiz
Q1: When using the Android embedded photo picker, which permission do you need to declare in your manifest for Android 14+ devices?
A: None. The embedded photo picker grants scoped URI access automatically without requiring READ_MEDIA_IMAGES or any other storage permission on Android 14+ (API 34+).
Q2: What’s the difference between collectAsState() and collectAsStateWithLifecycle() in Jetpack Compose?
A: collectAsState() collects the Flow continuously regardless of the app’s lifecycle state. collectAsStateWithLifecycle() pauses collection when the lifecycle drops below STARTED (e.g., when the app is backgrounded), saving resources.
Q3: What is remember {} without keys a risk for in Compose?
A: The computation inside remember {} only runs once during the composable’s lifetime. If the block captures variables that change on recomposition, the result becomes stale. Always list captured variables as keys: remember(key1, key2) { ... }.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
Jesse Wilson — “Don’t Block Inside Suspend Functions”: Jesse makes the case for why blocking calls inside suspend functions are worse than regular blocking calls. The coroutine scheduler treats suspend functions as cheap, cooperative operations — call Thread.sleep() or do blocking I/O inside one and you’ve pinned a thread from the shared pool. The fix is always withContext(Dispatchers.IO) { }.
Simon Vergauwen — “Exposed 1.0: Stable at Last”: JetBrains-backed Kotlin SQL library Exposed reached 1.0, bringing a stable public API contract and a commitment of no breaking changes until 2.0. Highlights include R2DBC reactive database support, improved type-safe schema migration DSL, and the removal of several deprecated APIs that have been lingering since 0.x.
Thomas Künneth — “Mapping Android’s Jetpack Stack onto Kotlin Multiplatform”: A practical guide to how standard Android Jetpack libraries (ViewModel, Room, DataStore, Navigation) map to their KMP-compatible equivalents. The article is particularly useful for teams starting their first KMP migration — it gives you a mental map before you start changing code.
Fragmented Podcast — “AI Coding Paradigms for Android Developers”: Kaushik Gopal and Iury Souza organize AI-assisted coding into four paradigms: autocomplete, natural language to code, test-driven generation, and agentic workflows. The framing helps experienced Android developers figure out where to invest their tooling attention first.
“Yet Another Rapid Compose (YARC) — Compose Shorthand Generator”: A new tool that generates Jetpack Compose layout shorthand. Less interesting as a production tool and more interesting as a study in Compose’s composability — it shows just how much of Android UI can now be expressed as small, composable functions.
“The Case Against Blocking in Coroutines — A Deep Dive”: A follow-up to Jesse Wilson’s post, this goes into the mechanics: why Dispatchers.Main has only one thread, why blocking it causes UI jank, and how suspend fun without withContext gives you false safety.
🛠️ Releases & Version Updates
Exposed 1.0.0 (Stable): JetBrains’ Kotlin SQL framework hits stable. The 1.0 release finalizes the Table/Column DSL API and adds R2DBC support for reactive database access:
// Exposed 1.0 — Type-safe table definitions (stable API)
object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 128)
val email = varchar("email", 256).uniqueIndex()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}
// Coroutine-safe transactions with suspendTransaction
suspend fun getUserByEmail(email: String): User? =
suspendTransaction {
Users.select { Users.email eq email }
.singleOrNull()
?.toUser()
}
Compose BOM 2026.01.00: The first BOM of 2026 ships with Compose UI 1.8.0-rc01, Material3 1.4.0-rc02, and Compose Runtime 1.8.0-rc01. The biggest runtime change: remember {} blocks with no keys now generate a compiler warning if the block captures mutable state.
// In build.gradle.kts — pin all Compose versions via BOM
implementation(platform("androidx.compose:compose-bom:2026.01.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// No version numbers needed — BOM handles alignment
Kotlinx.serialization 1.8.0: Adds experimental support for @SerialName on type parameters and improved error messages when deserializing unexpected JSON keys. The Json { ignoreUnknownKeys = true } behavior is now configurable at the property level.
Hilt 2.55: Minor release fixing lifecycle-aware ViewModel injection in Compose navigation. The fix prevents double-initialization when a HiltViewModel is used inside a NavHost with a rememberNavController() that survives configuration changes.
Compose BOM 2026.01 — What’s Actually New
The most developer-visible change in this BOM cycle is in Compose Foundation — BasicTextField now supports cursor blinking animations and IME action forwarding out of the box:
@Composable
fun SearchField(query: String, onQueryChange: (String) -> Unit) {
BasicTextField(
value = query,
onValueChange = onQueryChange,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = { /* handle search */ }
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
if (query.isEmpty()) Text("Search…", color = MaterialTheme.colorScheme.onSurfaceVariant)
innerTextField()
}
}
)
}
📱 Android Platform Updates
Android 16 QPR2 — Minor SDK Versioning in Practice
The minor SDK versioning system introduced in Android 16 QPR2 is now live in the wild. The practical impact: apps can start checking for QPR-specific APIs without waiting for the annual SDK bump.
// New helper — reads both major and minor SDK version
fun isAndroid16QPR2OrHigher(): Boolean =
Build.VERSION.SDK_INT >= 36 && Build.VERSION.SDK_INT_MINOR >= 1
// Use new QPR2 health background monitoring APIs
fun setupBackgroundHealthMonitoring(context: Context) {
if (isAndroid16QPR2OrHigher()) {
val healthManager = context.getSystemService(HealthServicesManager::class.java)
// QPR2-specific API surface
healthManager?.requestPassiveMonitoring(
PassiveMonitoringRequest.Builder()
.setDataTypes(setOf(DataType.HEART_RATE_BPM))
.build()
)
}
}
Android Security Bulletin — January 2026
Google published the January 2026 security bulletin with patches for 38 vulnerabilities. Three critical RCE vulnerabilities in the media framework (CVE-2026-0112, CVE-2026-0113, CVE-2026-0114) affect apps that parse media from untrusted sources. Update your Pixel and test your media pipelines.
🌟 Community Spotlight
Exposed 1.0 adoption in KMP projects: With Exposed 1.0 out the door, several open-source KMP projects have already started migrating from SQLDelight. The main differentiator: Exposed’s DSL is closer to Kotlin idioms, while SQLDelight generates type-safe Kotlin from .sq files. Worth knowing both.
YARC — Yet Another Rapid Compose: A new open-source tool for generating Compose layouts from shorthand syntax. Still early, but the shorthand approach is interesting for teams building internal tooling or design systems. Worth starring on GitHub if you follow Compose tooling.
💡 Quick Tip of the Week
Use withContext instead of launch(Dispatchers.IO) for simple background work:
// ❌ Creates a new coroutine, can complete after the parent is cancelled
fun loadUser(id: String) {
viewModelScope.launch(Dispatchers.IO) {
val user = userRepository.getUser(id)
_uiState.value = UiState.Success(user)
}
}
// ✅ Switches context within the same coroutine — respects cancellation
fun loadUser(id: String) {
viewModelScope.launch {
val user = withContext(Dispatchers.IO) {
userRepository.getUser(id)
}
_uiState.value = UiState.Success(user)
}
}
The launch(Dispatchers.IO) version runs its body even after viewModelScope is cancelled (e.g., after the ViewModel is cleared). withContext is structured — cancellation propagates correctly.
🧠 Did You Know?
suspend functions are NOT automatically non-blocking. The suspend keyword only tells the compiler to allow calling other suspend functions and to pause execution. It does nothing to prevent blocking calls. This compiles without any warning:
// This LOOKS safe but blocks the coroutine dispatcher's thread
suspend fun getUserProfile(id: String): UserProfile {
Thread.sleep(3000) // 🚫 blocks thread — violates cooperative scheduling
return UserProfile(id, "Mukul")
}
// This IS safe — moves blocking work to an IO thread
suspend fun getUserProfile(id: String): UserProfile {
return withContext(Dispatchers.IO) {
Thread.sleep(3000) // ok here — IO dispatcher has many threads
UserProfile(id, "Mukul")
}
}
The Kotlin compiler will happily let you block inside a suspend function. The correctness is entirely your responsibility — which is why Jesse Wilson’s post this week is worth bookmarking.
❓ Weekly Quiz
Q1: What does the suspend keyword guarantee about a function’s execution?
A: It allows the function to be paused and resumed by the coroutine scheduler, and allows it to call other suspend functions. It does NOT guarantee non-blocking behavior — you can still block a thread inside a suspend function.
Q2: What is the main benefit of the new minor SDK versioning introduced in Android 16 QPR2?
A: New APIs can ship mid-year in a quarterly release without requiring a full annual SDK version bump. Apps can check Build.VERSION.SDK_INT_MINOR to conditionally use QPR-specific APIs.
Q3: What stability guarantee does Exposed 1.0 provide that previous versions did not?
A: A stable public API contract — no breaking changes until version 2.0. The pre-1.0 releases had frequent breaking API changes that made version pinning risky.
That’s a wrap for this week! See you in the next issue. 🐝
📚 Articles & References
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.
🧪 Kotlin Coroutines 1.10 — What’s New
The biggest coroutines release in a while. Here’s what stands out:
Flow merge Operator
Combines 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)
}
Improved limitedParallelism
The 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()
}
Cancellation Cause Chain
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
}
}
}
🛠️ Releases & Version Updates
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.
📱 Android Platform Updates
Android 16 Minor SDK Versions
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)
}
Foldable & Large Screen Improvements
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()
}
}
}
🎤 Conferences & Videos
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.
🔒 Security & Vulnerability Alerts
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.
⚠️ Deprecation & Migration Alerts
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() } }
}
}
🧰 Tool of the Week
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.
🔎 AOSP Spotlight
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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 — What’s New
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.
Coroutine Workers as Default
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()
}
}
}
New Constraint Builder DSL
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
)
WorkManager + Hilt Integration
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
}
}
}
🛠️ Releases & Version Updates
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.
OkHttp 5.1 Highlights
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()
📱 Android Platform Updates
Android Studio Ladybug — Feature Highlight
Ladybug (2024.2.x) has been out for a few months, but many teams haven’t explored its best features:
- Live Edit: Changes to Compose code reflect in the running app within seconds — no rebuild. Works for
@Composablefunctions, modifiers, and string resources - Compose Preview Gallery: A new view mode that renders all
@Previewcomposables in a scrollable gallery. Faster than switching between previews one at a time - Gemini Integration: In-editor AI assistance for code generation, refactoring suggestions, and explaining complex code blocks. Opt-in via Settings → Gemini
- Device Mirror: Mirror a connected physical device directly in the IDE. Interact with the device without looking at the phone
Play Store Security Enforcement Update
Google announced updated enforcement timelines:
- targetSdk 35 required for all new app submissions starting January 31, 2026
- Credential Manager migration — apps using the deprecated
AccountManagerAPI for auth must migrate toCredentialManagerby March 2026 - Privacy Sandbox ads — apps using legacy advertising ID must adopt the Privacy Sandbox APIs by Q3 2026
🎤 Conferences & Videos
KotlinConf 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.
🔒 Security & Vulnerability Alerts
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
)
}
🌟 Community Spotlight
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.
🧰 Tool of the Week
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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 LTS — What’s New
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:
Stable Guard Conditions in when
Guard 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)
}
}
Stable Context Parameters
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()
}
Improved K2 Compiler Performance
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%.
🛠️ Releases & Version Updates
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 Highlights
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)
}
}
}
📱 Android Platform Updates
Android Gradle Plugin 9.0 — The Performance Release
AGP 9.0 is the biggest build performance jump in recent memory. The key changes:
- Configuration cache is now on by default — no more opt-in. If your build breaks, you likely have a plugin that isn’t compatible yet
- Task avoidance — AGP now skips more tasks during incremental builds, especially for resource-only changes
- New dependency resolution — Gradle’s new resolution engine reduces sync time on multi-module projects
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
2025 Year-in-Review: Android by Numbers
- Compose adoption: Used in 70%+ of top 1000 Play Store apps (up from 40% in 2024)
- Kotlin Multiplatform: 3x growth in production KMP apps on Play Store
- Android 16: First release under the new quarterly cadence — QPR1 and QPR2 both shipped on schedule
- Target SDK requirement: Google now requires
targetSdk 35for new app submissions
🎤 Conferences & Videos
Droidcon 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.
🧰 Tool of the Week
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.
⚠️ Deprecation & Migration Alerts
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→ UseenableEdgeToEdge()from Activity 1.8+accompanist-pager→ UseHorizontalPager/VerticalPagerfrom Compose Foundationaccompanist-permissions→ Use the official permissions API from Activity Compose
AGP 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.
🔎 AOSP Spotlight
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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.
🧪 Compose December ‘25 Release — What’s New
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 Recompositions
The 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")
}
Improved AnimatedContent Transitions
AnimatedContent 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)
}
}
}
New 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)
}
}
}
🛠️ Releases & Version Updates
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 Highlights
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")
}
}
📱 Android Platform Updates
XR Libraries for Spatial Computing
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)
}
}
}
Android Studio Profiler Updates
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.
🧰 Tool of the Week
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.
🔎 AOSP Spotlight
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.
🌟 Community Spotlight
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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 — Top Talks
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.
🛠️ Releases & Version Updates
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.
Paging 4 API Preview
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)
}
}
}
📱 Android Platform Updates
Compose UI Testing Improvements
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
)
Credential Manager Updates
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 Theming — Dynamic Color Deep Dive
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
)
}
Custom Color Roles
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
)
}
🌟 Community Spotlight
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.
🧰 Tool of the Week
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")
}
}
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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 Developer Preview 2 — What’s New
Android 16 DP2 brings a few APIs worth exploring early. Here are the highlights:
Granular Notification Permissions
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"
)
}
App-Specific Language Preferences API
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")
Improved Photo Picker with Ordering
The photo picker now returns results in the order the user selected them — a long-requested feature for apps that need ordered media selection.
🛠️ Releases & Version Updates
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.
Room KMP Setup
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()
}
📱 Android Platform Updates
Privacy Sandbox — Attribution Reporting
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)
}
}
Topics API for Interest-Based Ads
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()
}
🎤 Conferences & Videos
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.
⚠️ Deprecation & Migration Alerts
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.
🌟 Community Spotlight
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.
🧰 Tool of the Week
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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.
🧪 AGP 8.8 — What’s New
R8 Full Mode by Default
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.*
Build Cache Improvements
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.
🛠️ Releases & Version Updates
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.
Compose Navigation Type Safety
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.
📱 Android Platform Updates
Google Play New Requirements
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")
}
}
}
}
CameraX Improvements
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()
}
🎤 Conferences & Videos
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.
🔒 Security & Vulnerability Alerts
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.
⚠️ Deprecation & Migration Alerts
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
}
}
🌟 Community Spotlight
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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 Global — Top Sessions
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) }
}
}
}
🛠️ Releases & Version Updates
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.
Molecule 2.0 API Changes
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
)
📱 Android Platform Updates
Health Connect Integration
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()
)
}
}
Edge-to-Edge is Now the Default
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))
}
}
}
}
}
🧰 Tool of the Week
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.
🔎 AOSP Spotlight
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.
🌟 Community Spotlight
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)
🔒 Security & Vulnerability Alerts
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.
💡 Quick Tip of the Week
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. 🐝
📚 Articles & References
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.
🧪 Compose 1.9 — What’s New
Stable Shared Element Transitions
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
)
}
}
}
}
Modifier.Node Performance
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
}
🛠️ Releases & Version Updates
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 3.1 Shared Element Transitions
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
)
}
}
📱 Android Platform Updates
AndroidX October Roundup
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.
Browser Custom Tabs Partial Height
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))
}
🔎 AOSP Spotlight
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.
🧰 Tool of the Week
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.
🌟 Community Spotlight
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.
💡 Quick Tip of the Week
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.
🧠 Did You Know?
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).
❓ Weekly Quiz
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. 🐝
📚 Articles & References
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.
🎤 Droidcon NYC — Top Sessions
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 1.0 — What’s New
Navigation with Circuit
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
)
}
Overlay (Bottom Sheets, Dialogs)
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()
}
}
}
)
}
🛠️ Releases & Version Updates
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 KSP Performance
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")
}
📱 Android Platform Updates
AGP 8.8 Build Cache
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 RC
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.
🌟 Community Spotlight
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.
🔒 Security & Vulnerability Alerts
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.
💡 Quick Tip of the Week
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.
🧠 Did You Know?
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.
❓ Weekly Quiz
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. 🐝
📚 Articles & References
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.
🧪 SQLDelight 2.1 — What’s New
Async Drivers for KMP
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
)
}
}
Window Functions in .sq Files
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 = ?;
🛠️ Releases & Version Updates
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%.
Compose Compiler Reports
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
}
📱 Android Platform Updates
Android 15 QPR2 Predictive Back
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 }
)
}
}
}
}
Baseline Profile Verification
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
}
}
}
🔎 AOSP Spotlight
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.
🧰 Tool of the Week
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.
💡 Quick Tip of the Week
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.
🧠 Did You Know?
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).
❓ Weekly Quiz
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. 🐝
📚 Articles & References
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 — What’s New
Automatic Retry Plugin
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 Experimental Support
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
}
}
🛠️ Releases & Version Updates
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.
WorkManager Foreground Service
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
)
}
}
📱 Android Platform Updates
Gradle 8.12 Configuration Cache
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 Typed Error Handling
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 }
)
}
🎤 Conferences & Videos
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.
🌟 Community Spotlight
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.
💡 Quick Tip of the Week
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.
🧠 Did You Know?
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.
❓ Weekly Quiz
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. 🐝
📚 Articles & References
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 — What’s New
KSP-Only Processing
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
}
Upsert Support
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()
)
Experimental KMP Support
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()
}
🛠️ Releases & Version Updates
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.
Paging 3.4 Error Handling
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() }
)
}
}
📱 Android Platform Updates
Compose State Inspector
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 Cold Start Optimization
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
}
🧰 Tool of the Week
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:
MutableStatereturned from ViewModel (should beState)Listparameter without@Immutablewrapperrememberwith a lambda that captures mutable external state- Missing
keyparameter inLazyColumn.items()
🌟 Community Spotlight
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.
💡 Quick Tip of the Week
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.
🧠 Did You Know?
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.
❓ Weekly Quiz
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. 🐝