compose-multiplatform-patterns — Claude Code skill
Patterns for Compose Multiplatform and Jetpack Compose in KMP projects, covering state, navigation, theming, performance, and platform-specific UI.
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a claude-codeWhat compose-multiplatform-patterns does
This skill guides shared UI work for Android, iOS, Desktop, and Web with Compose Multiplatform and Jetpack Compose. It covers ViewModel with a single StateFlow state object, collecting state in Compose, event sinks, type-safe navigation, dialogs, and bottom sheets.
The material also shows slot-based composables, modifier ordering, expect/actual platform UI, stable types, LazyColumn keys, derivedStateOf, and ways to reduce unnecessary recomposition. Material 3 and dynamic theming are included. Use it when an agent is building or refactoring Compose UI in a KMP or Android project. Skip it for projects that do not use Compose.
What it helps with
- model screen state with ViewModel and StateFlow
- add type-safe navigation to a KMP project
- separate platform UI with expect/actual
- reduce unnecessary recomposition in LazyColumn
- build reusable composables with slot APIs
Example prompts for the agent
- Add an item list screen with ViewModel, StateFlow, and search.
- Set up type-safe navigation for HomeRoute and DetailRoute.
- Review this LazyColumn and reduce unnecessary recomposition.
How to install compose-multiplatform-patterns
1Skills CLI (Vercel)
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a claude-code2Manually: put the files in place
~/.claude/skills/compose-multiplatform-patterns/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/compose-multiplatform-patterns/
1Skills CLI (Vercel)
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a codex2Manually: put the files in place
.agents/skills/compose-multiplatform-patterns/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a cursor2Manually: put the files in place
.cursor/skills/compose-multiplatform-patterns/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install affaan-m/ECC compose-multiplatform-patterns2Manually: put the files in place
.github/skills/compose-multiplatform-patterns/SKILL.md1Skills CLI (Vercel)
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a gemini-cli2Platform CLI
gemini skills install https://github.com/affaan-m/ECC --path skills/compose-multiplatform-patternsManually: put the files in place
.devin/skills/compose-multiplatform-patterns/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set146
The whole set →Similar
Questions about compose-multiplatform-patterns
How do I install compose-multiplatform-patterns in Claude Code?
Run npx skills add affaan-m/ECC --skill compose-multiplatform-patterns -a claude-code. The skill file will be placed at ~/.claude/skills/compose-multiplatform-patterns/SKILL.md.
What does compose-multiplatform-patterns do?
It provides patterns for Compose UI state, navigation, theming, performance, and platform-specific code in KMP projects, with examples for Android, iOS, Desktop, and Web.
What projects is this skill for?
It is for Compose Multiplatform or Jetpack Compose projects that use ViewModel, StateFlow, type-safe navigation, reusable composables, or recomposition tuning.
Source text
SKILL.md · MIT
---
name: compose-multiplatform-patterns
description: Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI. Use when building Compose or Jetpack Compose UI, state, navigation, or theming in a KMP project.
metadata:
origin: ECC
---
# Compose Multiplatform Patterns
Patterns for building shared UI across Android, iOS, Desktop, and Web using Compose Multiplatform and Jetpack Compose. Covers state management, navigation, theming, and performance.
## When to Activate
- Building Compose UI (Jetpack Compose or Compose Multiplatform)
- Managing UI state with ViewModels and Compose state
- Implementing navigation in KMP or Android projects
- Designing reusable composables and design systems
- Optimizing recomposition and rendering performance
## State Management
### ViewModel + Single State Object
Use a single data class for screen state. Expose it as `StateFlow` and collect in Compose:
```kotlin
data class ItemListState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val searchQuery: String = ""
)
class ItemListViewModel(
private val getItems: GetItemsUseCase
) : ViewModel() {
private val _state = MutableStateFlow(ItemListState())
val state: StateFlow<ItemListState> = _state.asStateFlow()
fun onSearch(query: String) {
_state.update { it.copy(searchQuery = query) }
loadItems(query)
}
private fun loadItems(query: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
getItems(query).fold(
onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } },
onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
)
}
}
}
```
### Collecting State in Compose
```kotlin
@Composable
fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
ItemListContent(
state = state,
onSearch = viewModel::onSearch
)
}
@Composable
private fun ItemListContent(
state: ItemListState,
onSearch: (String) -> Unit
) {
// Stateless composable — easy to preview and test
}
```
### Event Sink Pattern
For complex screens, use a sealed interface for events instead of multiple callback lambdas:
```kotlin
sealed interface ItemListEvent {
data class Search(val query: String) : ItemListEvent
data class Delete(val itemId: String) : ItemListEvent
data object Refresh : ItemListEvent
}
// In ViewModel
fun onEvent(event: ItemListEvent) {
when (event) {
is ItemListEvent.Search -> onSearch(event.query)
is ItemListEvent.Delete -> deleteItem(event.itemId)
is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery)
}
}
// In Composable — single lambda instead of many
ItemListContent(
state = state,
onEvent = viewModel::onEvent
)
```
## Navigation
### Type-Safe Navigation (Compose Navigation 2.8+)
Define routes as `@Serializable` objects:
```kotlin
@Serializable data object HomeRoute
@Serializable data class DetailRoute(val id: String)
@Serializable data object SettingsRoute
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
}
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(id = route.id)
}
composable<SettingsRoute> { SettingsScreen() }
}
}
```
### Dialog and Bottom Sheet Navigation
Use `dialog()` and overlay patterns instead of imperative show/hide:
```kotlin
NavHost(navController, startDestination = HomeRoute) {
composable<HomeRoute> { /* ... */ }
dialog<ConfirmDeleteRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
ConfirmDeleteDialog(
itemId = route.itemId,
onConfirm = { navController.popBackStack() },
onDismiss = { navController.popBackStack() }
)
}
}
```
## Composable Design
### Slot-Based APIs
Design composables with slot parameters for flexibility:
```kotlin
@Composable
fun AppCard(
modifier: Modifier = Modifier,
header: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit,
actions: @Composable RowScope.() -> Unit = {}
) {
Card(modifier = modifier) {
Column {
header()
Column(content = content)
Row(horizontalArrangement = Arrangement.End, content = actions)
}
}
}
```
### Modifier Ordering
Modifier order matters — apply in this sequence:
```kotlin
Text(
text = "Hello",
modifier = Modifier
.padding(16.dp) // 1. Layout (padding, size)
.clip(RoundedCornerShape(8.dp)) // 2. Shape
.background(Color.White) // 3. Drawing (background, border)
.clickable { } // 4. Interaction
)
```
## KMP Platform-Specific UI
### expect/actual for Platform Composables
```kotlin
// commonMain
@Composable
expect fun PlatformStatusBar(darkIcons: Boolean)
// androidMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
val systemUiController = rememberSystemUiController()
SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }
}
// iosMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
// iOS handles this via UIKit interop or Info.plist
}
```
## Performance
### Stable Types for Skippable Recomposition
Mark classes as `@Stable` or `@Immutable` when all properties are stable:
```kotlin
@Immutable
data class ItemUiModel(
val id: String,
val title: String,
val description: String,
val progress: Float
)
```
### Use `key()` and Lazy Lists Correctly
```kotlin
LazyColumn {
items(
items = items,
key = { it.id } // Stable keys enable item reuse and animations
) { item ->
ItemRow(item = item)
}
}
```
### Defer Reads with `derivedStateOf`
```kotlin
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
```
### Avoid Allocations in Recomposition
```kotlin
// BAD — new lambda and list every recomposition
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
// GOOD — key each item so callbacks stay attached to the right row
val activeItems = remember(items) { items.filter { it.isActive } }
activeItems.forEach { item ->
key(item.id) {
ActiveItem(item, onClick = { handle(item) })
}
}
```
## Theming
### Material 3 Dynamic Theming
```kotlin
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
else dynamicLightColorScheme(LocalContext.current)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(colorScheme = colorScheme, content = content)
}
```
## Anti-Patterns to Avoid
- Using `mutableStateOf` in ViewModels when `MutableStateFlow` with `collectAsStateWithLifecycle` is safer for lifecycle
- Passing `NavController` deep into composables — pass lambda callbacks instead
- Heavy computation inside `@Composable` functions — move to ViewModel or `remember {}`
- Using `LaunchedEffect(Unit)` as a substitute for ViewModel init — it re-runs on configuration change in some setups
- Creating new object instances in composable parameters — causes unnecessary recomposition
## References
See skill: `android-clean-architecture` for module structure and layering.
See skill: `kotlin-coroutines-flows` for coroutine and Flow patterns.
© affaan-m · MIT · affaan-m/ECC