Claude
Skills
Sign in
Back

jetpack-compose

Included with Lifetime
$97 forever

Jetpack Compose for native Android UI. Covers @Composable functions, state, side effects, ViewModel + Compose, Hilt DI, Navigation Compose, Material 3 with Dynamic Color (Material You), Compose for Wear OS, Compose previews, and Android lifecycle integration (Activity, Fragment interop). USE WHEN: user mentions "Jetpack Compose", "@Composable" in Android-only context, "ViewModel", "Hilt", "Navigation Compose", "Material You", "Dynamic Color", "Compose preview", "AndroidView", "rememberLauncherForActivityResult", "Compose Wear OS" DO NOT USE FOR: Cross-platform Compose - use `frontend-frameworks/compose-multiplatform` DO NOT USE FOR: Kotlin language fundamentals - use `languages/kotlin` DO NOT USE FOR: Android non-UI APIs (Keystore, NFC, etc) - use `mobile/android-native`

Design

What this skill does

# Jetpack Compose (Android)

> **References**: [state-effects.md](quick-ref/state-effects.md) for ViewModel + Compose, side-effect APIs, snapshot system. [navigation.md](quick-ref/navigation.md) for Navigation Compose 2.8+ with type-safe routes, deep links, multi-stack. [interop.md](quick-ref/interop.md) for AndroidView/ComposeView interop, Activity Result Contracts, Fragment integration.
>
> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `jetpack-compose`.

## Setup

```kotlin
// app/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android") version "2.2.0"
    id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"   // required for Kotlin 2.x
    id("com.google.devtools.ksp")
    id("dagger.hilt.android.plugin")
}

android {
    buildFeatures { compose = true }
    composeOptions {
        // Compose Compiler is now part of Kotlin 2.x — no kotlinCompilerExtensionVersion needed
    }
}

dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2025.01.00")
    implementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.material:material-icons-extended")
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    implementation("androidx.activity:activity-compose:1.10.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")
    implementation("androidx.navigation:navigation-compose:2.8.5")
    implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
    implementation("com.google.dagger:hilt-android:2.55")
    ksp("com.google.dagger:hilt-compiler:2.55")
}
```

## Activity Entry Point

```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()                         // draws under status/nav bars
        setContent {
            BhodlTheme {
                AppNavHost()
            }
        }
    }
}
```

`enableEdgeToEdge()` (Activity 1.8+) is the modern way to configure edge-to-edge — replaces `WindowCompat.setDecorFitsSystemWindows(window, false)`.

## ViewModel + StateFlow + Compose

```kotlin
@HiltViewModel
class WalletViewModel @Inject constructor(
    private val repo: WalletRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val walletId: String = savedStateHandle["walletId"] ?: error("missing walletId")

    private val _state = MutableStateFlow<UiState>(UiState.Loading)
    val state: StateFlow<UiState> = _state.asStateFlow()

    init { load() }

    fun load() {
        viewModelScope.launch {
            _state.value = UiState.Loading
            runCatching { repo.getWallet(walletId) }
                .onSuccess { _state.value = UiState.Success(it) }
                .onFailure { _state.value = UiState.Error(it.message ?: "unknown") }
        }
    }
}

@Composable
fun WalletScreen(
    viewModel: WalletViewModel = hiltViewModel(),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    when (val s = state) {
        UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> WalletContent(s.wallet, onRefresh = viewModel::load)
        is UiState.Error -> ErrorView(s.message, onRetry = viewModel::load)
    }
}
```

`collectAsStateWithLifecycle()` (from `lifecycle-runtime-compose`) is **preferred over `collectAsState`** — automatically pauses collection when screen is in background, prevents wasted work and battery drain.

## Material 3 + Dynamic Color (Material You)

```kotlin
@Composable
fun BhodlTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,                  // Material You on Android 12+
    content: @Composable () -> Unit,
) {
    val context = LocalContext.current
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        darkTheme -> DarkColors
        else -> LightColors
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = bhodlTypography,
        shapes = bhodlShapes,
        content = content,
    )
}
```

For status/nav bar tinting in edge-to-edge:

```kotlin
val view = LocalView.current
if (!view.isInEditMode) {
    SideEffect {
        val window = (view.context as Activity).window
        WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
    }
}
```

## Navigation Compose (Type-Safe Routes — 2.8+)

```kotlin
@Serializable
data object HomeRoute

@Serializable
data class WalletDetailRoute(val walletId: String)

@Composable
fun AppNavHost() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = HomeRoute,
    ) {
        composable<HomeRoute> {
            HomeScreen(
                onWalletClick = { id -> navController.navigate(WalletDetailRoute(id)) },
            )
        }
        composable<WalletDetailRoute> { backStackEntry ->
            val route: WalletDetailRoute = backStackEntry.toRoute()
            WalletDetailScreen(
                walletId = route.walletId,
                onBack = { navController.popBackStack() },
            )
        }
    }
}
```

Type-safe routes (since Navigation Compose 2.8) replace string-based routes — no more typo-driven crashes.

For nested graphs:

```kotlin
@Serializable data object SettingsGraph
@Serializable data object ProfileSettingsRoute

NavHost(navController = nav, startDestination = HomeRoute) {
    navigation<SettingsGraph>(startDestination = ProfileSettingsRoute) {
        composable<ProfileSettingsRoute> { ProfileSettings() }
        composable<NotificationSettingsRoute> { NotificationSettings() }
    }
}
```

See [navigation.md](quick-ref/navigation.md) for deep linking, multi-stack bottom nav, dialog/bottom-sheet destinations.

## Side Effects (Android-specific)

| API | Use case |
|---|---|
| `LaunchedEffect(key)` | Suspending work, cancels on key change |
| `DisposableEffect(key)` | Setup with cleanup (lifecycle observer, BroadcastReceiver) |
| `LifecycleEventEffect(event)` | React to specific Lifecycle events (ON_RESUME, ON_PAUSE) |
| `LifecycleResumeEffect` / `LifecycleStartEffect` | Lifecycle-scoped effect with auto-pause |
| `rememberLauncherForActivityResult` | Permissions, file picker, take photo |
| `BackHandler { }` | Intercept system back button |

```kotlin
@Composable
fun CameraScreen() {
    val context = LocalContext.current

    val cameraPermissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { granted -> /* ... */ },
    )

    LaunchedEffect(Unit) {
        cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

@Composable
fun ProcessLifecycleObserver(onResume: () -> Unit) {
    LifecycleResumeEffect(Unit) {
        onResume()
        onPauseOrDispose { /* cleanup */ }
    }
}

@Composable
fun ConfirmExit(onExit: () -> Unit) {
    BackHandler { onExit() }
}
```

## Hilt + Compose

```kotlin
// Module
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides @Singleton
    fun provideWalletRepository(api: WalletApi, db: AppDatabase): WalletRepository =
        WalletRepositoryImpl(api, db)
}

// ViewModel
@HiltViewModel
class WalletViewModel @Inject constructor(
    private val repo: WalletRepository,
) : ViewModel() { /* ... */ }

// Composable injection
@Composable
fun WalletScreen(viewModel: WalletViewModel = hiltViewModel()) { /* ... */ }
```

For nested `NavHost` with Hilt-scoped ViewModel:

```kotlin
@Composable
fun NestedScreen(navBackStackEntr

Related in Design