# Kotlin Compose

> When to activate: Jetpack Compose, @Composable, State, remember, LaunchedEffect, navigation, ViewModel, Compose UI, recomposition

- Skill: `mattakushi432/kotlin-compose` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/kotlin-compose`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/kotlin-compose/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/kotlin-compose

---

# Jetpack Compose Patterns

## State & Recomposition

```kotlin
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { count++ }) { Text("Increment") }
    }
}

// Hoist state for reusability
@Composable
fun CounterScreen(viewModel: CounterViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    CounterContent(count = state.count, onIncrement = viewModel::increment)
}

@Composable
fun CounterContent(count: Int, onIncrement: () -> Unit) {
    // Pure, testable, no ViewModel dependency
    Column {
        Text("Count: $count")
        Button(onClick = onIncrement) { Text("Increment") }
    }
}
```

## ViewModel Integration

```kotlin
data class UserUiState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val error: String? = null
)

@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository,
    savedState: SavedStateHandle
) : ViewModel() {

    private val userId = savedState.get<Long>("userId")!!

    private val _state = MutableStateFlow(UserUiState(isLoading = true))
    val state: StateFlow<UserUiState> = _state.asStateFlow()

    init {
        loadUser()
    }

    private fun loadUser() = viewModelScope.launch {
        _state.update { it.copy(isLoading = true, error = null) }
        runCatching { repo.findById(userId) }
            .onSuccess { user -> _state.update { it.copy(user = user, isLoading = false) } }
            .onFailure { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
    }
}
```

## Side Effects

```kotlin
@Composable
fun SearchScreen(query: String) {
    // LaunchedEffect — runs when key changes, cancels previous
    LaunchedEffect(query) {
        delay(300) // debounce
        performSearch(query)
    }

    // DisposableEffect — cleanup on leave
    val lifecycle = LocalLifecycleOwner.current
    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) refresh()
        }
        lifecycle.lifecycle.addObserver(observer)
        onDispose { lifecycle.lifecycle.removeObserver(observer) }
    }

    // rememberCoroutineScope — for event-driven launches (not initial load)
    val scope = rememberCoroutineScope()
    Button(onClick = { scope.launch { saveData() } }) { Text("Save") }
}
```

## Navigation

```kotlin
@Composable
fun AppNavGraph(navController: NavHostController) {
    NavHost(navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onUserClick = { id -> navController.navigate("user/$id") })
        }
        composable(
            "user/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.LongType })
        ) { backStack ->
            val userId = backStack.arguments!!.getLong("userId")
            UserDetailScreen(userId = userId, onBack = navController::popBackStack)
        }
    }
}
```

## Lists & Performance

```kotlin
@Composable
fun UserList(users: List<User>, onUserClick: (Long) -> Unit) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(users, key = { it.id }) { user ->  // key prevents full recomposition
            UserCard(user = user, onClick = { onUserClick(user.id) })
        }
    }
}

// Stable class prevents unnecessary recomposition
@Stable
data class UserUiModel(val id: Long, val name: String, val avatarUrl: String)
```

## Custom Modifiers & Theming

```kotlin
fun Modifier.shimmer(): Modifier = composed {
    val infiniteTransition = rememberInfiniteTransition()
    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.2f, targetValue = 1f,
        animationSpec = infiniteRepeatable(tween(800), RepeatMode.Reverse)
    )
    alpha(alpha).background(MaterialTheme.colorScheme.surfaceVariant)
}

// Material 3 theming
MaterialTheme(
    colorScheme = if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme,
    typography = AppTypography,
    content = content
)
```

## Key Rules
- Hoist state up to the lowest common ancestor — composables that don't own state are easier to test and preview
- Use `key = { item.id }` in `LazyColumn` items — prevents full list recomposition on data changes
- `LaunchedEffect(Unit)` runs once on composition; `LaunchedEffect(key)` reruns when key changes
- Mark data classes as `@Stable` or `@Immutable` when Compose can't infer stability — reduces unnecessary recomposition
- Never do I/O or heavy computation directly in a composable — always delegate to ViewModel or a coroutine

