Android Development with Compose and Material 3 Expressive
Modern Android development with Jetpack Compose as the default UI toolkit and Material 3 Expressive as the default design direction, a dated snapshot. Covers Compose fundamentals (composables, modifiers, state hoisting, side effects, lazy lists, strong skipping, recomposition), Material 3 and Expressive theming and components (MaterialExpressiveTheme, MotionScheme, button groups, split buttons, FAB menu, floating toolbars, wavy progress indicators, shape morphing, dynamic color), architecture (ViewModel, StateFlow, collectAsStateWithLifecycle, Hilt), type-safe Navigation Compose and Navigation 3, data (Room, DataStore, Retrofit, Ktor, Coil), the Gradle Kotlin DSL build with version catalogs and KSP, testing, performance, and View interop. Use when building Android UI in Compose, theming with Material 3 or Expressive, wiring architecture or navigation, configuring the build, or debugging Compose issues. Check the project's versions first; re-verify version facts against the official Android and AndroidX docs.
This skill defaults new Android UI to Jetpack Compose and Material 3 Expressive as the design direction, captured as a dated snapshot. The paradigms (declarative UI, state hoisting, unidirectional data flow, Material 3 theming) move slowly. The exact version numbers, the Compose compiler setup, and which Expressive APIs are stable move fast, so those facts are dated and meant to be re-verified.
Snapshot and freshness
- Snapshot date: 2026-06-15 (the
datein frontmatter). Targets Compose BOM 2026.06.00 (core Compose 1.11.x,material31.4.0), Kotlin 2.4.0, Android 16 / API 36. Each reference file carries its ownVerifieddate. - Material 3 Expressive is the design direction, not yet a stable API. As of this snapshot the Expressive components live in
androidx.compose.material3:material31.5.0-alpha behind@ExperimentalMaterial3ExpressiveApi; stablematerial3is 1.4.0 (baseline M3). Default to Expressive intent, opt into the experimental APIs explicitly, and fall back to stable baseline M3 where you need stable surface. Re-check whether 1.5.0 has gone stable before assuming it has. See references/material3-expressive.md. - Check the project's versions first. Read
gradle/libs.versions.tomland the modulebuild.gradle.ktsfor the Compose BOM,material3, Kotlin, AGP,compileSdk/targetSdk. The installed versions decide which APIs and pitfalls apply. Never assume from memory. - Staleness rule. If today is well past a file's
Verifieddate, or a new Compose BOM / Kotlin / Android major has shipped, treat its version-specific claims (artifact versions, "experimental vs stable", breaking changes) as suspect and confirm against the AndroidX release notes. The paradigm sections age far slower. - Updating. Confirm against the official docs (developer.android.com, the AndroidX release notes, m3.material.io, kotlinlang.org), refresh the file, and bump its
Verifiedline. Maintainers: see MAINTENANCE.md.
Core Rules
- Compose-first. Build new UI in Compose, not XML Views. Reach for View interop only to embed an unported widget or migrate incrementally.
- Hoist state, flow data one way. Composables take state down as parameters and send events up as lambdas. Keep UI state in a
ViewModelas an immutable data class exposed viaStateFlow. - Write as if Strong Skipping is on (it is, since Kotlin 2.0.20). Don't reflexively wrap everything in
remember/derivedStateOf. Pass stable types and@Immutabledata; memoize only when profiling says so. - Material 3 Expressive is the design direction. Theme with
MaterialExpressiveTheme, motion withMotionScheme, reach for the expressive components, but keep the experimental opt-in honest. See references/material3-expressive.md. - Apply the Compose compiler via the Kotlin Gradle plugin (
org.jetbrains.kotlin.plugin.compose), versioned with Kotlin. The oldcomposeOptions { kotlinCompilerExtensionVersion }is obsolete. - Collect flows with
collectAsStateWithLifecycle(), notcollectAsState(), so collection pauses in the background. - KSP, not kapt. Use the Gradle Kotlin DSL with a
libs.versions.tomlversion catalog. Go edge-to-edge by default (enableEdgeToEdge(); enforced attargetSdk35+).
Project Structure Convention
app/src/main/java/com/example/app/MainActivity.kt enableEdgeToEdge() + setContent { AppTheme { ... } }ui/theme/ Color.kt, Type.kt, Shape.kt, Theme.kt (MaterialExpressiveTheme)<feature>/ screen composables + per-feature ViewModel + UiStatecomponents/ reusable composablesdata/<feature>/ repository + data sources (Room, DataStore, network)di/ Hilt modulesnavigation/ type-safe routes + NavHost (or Nav3 NavDisplay)gradle/libs.versions.toml the version catalog (single source of versions)
Reference Files
Consult these based on the task. Each carries its own Verified date.
Compose core
- Composables, modifiers and order, layouts, lazy lists with keys, state (remember, rememberSaveable, derivedStateOf, produceState), state hoisting, side effects (LaunchedEffect, DisposableEffect, rememberCoroutineScope, snapshotFlow), previews - See references/compose.md
- App architecture, UI/domain/data layers, UDF, immutable UI state, ViewModel, StateFlow, collectAsStateWithLifecycle, Hilt dependency injection - See references/architecture.md
- Recomposition, stability and @Immutable, strong skipping, deferring state reads with lambda modifiers, compiler metrics, Layout Inspector, baseline profiles, Macrobenchmark - See references/performance.md
Design and Material 3 Expressive
- Material 3 Expressive status and opt-in, MaterialExpressiveTheme, MotionScheme, button groups, toggle and split buttons, FAB menu, floating toolbars, loading and wavy progress indicators, MaterialShapes shape morphing - See references/material3-expressive.md
- ColorScheme and color roles, dynamic color (Material You), typography type scale, shapes, dark theme, edge-to-edge and window insets - See references/theming.md
Navigation and data
- Type-safe Navigation Compose (@Serializable routes,
composable<T>, toRoute), nested graphs, passing arguments, ViewModel scoping, and Navigation 3 (you own the back stack) - See references/navigation.md - Data layer, Room with KSP, DataStore over SharedPreferences, Retrofit or Ktor with kotlinx.serialization, Coil 3 image loading, repositories, coroutines and Flow - See references/data.md
Build, test, interop, pitfalls
- Gradle Kotlin DSL, the libs.versions.toml version catalog, the Compose compiler plugin, KSP, build variants, R8, AGP and Android Studio, building and verifying from the CLI with gradlew - See references/tooling.md
- Compose UI testing, semantics and test tags, finders and assertions, Turbine for flows, screenshot testing, MockK and Robolectric - See references/testing.md
- Compose and View interop, AndroidView and ComposeView, ViewCompositionStrategy, incremental migration from XML - See references/interop.md
- Common pitfalls, deprecated API, version-tagged breaking changes and migrations (kapt to KSP, composeOptions to the compiler plugin, collectAsState, Accompanist, edge-to-edge enforcement) - See references/pitfalls.md
Quick Patterns
Stateful screen + stateless content (state hoisting)
kotlin@Composablefun CounterScreen(viewModel: CounterViewModel = hiltViewModel()) {val state by viewModel.uiState.collectAsStateWithLifecycle()CounterContent(count = state.count, onIncrement = viewModel::increment)}@Composablefun CounterContent(count: Int, onIncrement: () -> Unit) {Button(onClick = onIncrement) { Text("Count: $count") }}
Immutable UI state exposed as StateFlow
kotlindata class CounterUiState(val count: Int = 0)class CounterViewModel : ViewModel() {private val _uiState = MutableStateFlow(CounterUiState())val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow()fun increment() = _uiState.update { it.copy(count = it.count + 1) }}
Material 3 Expressive theme (experimental opt-in)
kotlin@OptIn(ExperimentalMaterial3ExpressiveApi::class)@Composablefun AppTheme(content: @Composable () -> Unit) {val scheme = if (isSystemInDarkTheme()) darkColorScheme() // no expressive dark variantelse expressiveLightColorScheme()MaterialExpressiveTheme(colorScheme = scheme, motionScheme = MotionScheme.expressive(), content = content)}
Lazy list with stable keys
kotlinLazyColumn {items(items = users, key = { it.id }) { user -> UserRow(user) }}
Type-safe navigation route
kotlin@Serializable data class Profile(val userId: String)composable<Profile> { backStackEntry ->val args = backStackEntry.toRoute<Profile>()ProfileScreen(userId = args.userId)}
