Claude Agent Skill · by Affaan M

Compose Multiplatform Patterns

Install Compose Multiplatform Patterns skill for Claude Code from affaan-m/everything-claude-code.

Install
Terminal · npx
$npx skills add https://github.com/obra/superpowers --skill brainstorming
Works with Paperclip

How Compose Multiplatform Patterns fits into a Paperclip company.

Compose Multiplatform Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md299 lines
Expand
---name: compose-multiplatform-patternsdescription: Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.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: ```kotlindata 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@Composablefun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {    val state by viewModel.state.collectAsStateWithLifecycle()     ItemListContent(        state = state,        onSearch = viewModel::onSearch    )} @Composableprivate 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: ```kotlinsealed interface ItemListEvent {    data class Search(val query: String) : ItemListEvent    data class Delete(val itemId: String) : ItemListEvent    data object Refresh : ItemListEvent} // In ViewModelfun 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 manyItemListContent(    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 @Composablefun 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: ```kotlinNavHost(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@Composablefun 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: ```kotlinText(    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@Composableexpect fun PlatformStatusBar(darkIcons: Boolean) // androidMain@Composableactual fun PlatformStatusBar(darkIcons: Boolean) {    val systemUiController = rememberSystemUiController()    SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }} // iosMain@Composableactual 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@Immutabledata class ItemUiModel(    val id: String,    val title: String,    val description: String,    val progress: Float)``` ### Use `key()` and Lazy Lists Correctly ```kotlinLazyColumn {    items(        items = items,        key = { it.id }  // Stable keys enable item reuse and animations    ) { item ->        ItemRow(item = item)    }}``` ### Defer Reads with `derivedStateOf` ```kotlinval listState = rememberLazyListState()val showScrollToTop by remember {    derivedStateOf { listState.firstVisibleItemIndex > 5 }}``` ### Avoid Allocations in Recomposition ```kotlin// BAD — new lambda and list every recompositionitems.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) } // GOOD — key each item so callbacks stay attached to the right rowval activeItems = remember(items) { items.filter { it.isActive } }activeItems.forEach { item ->    key(item.id) {        ActiveItem(item, onClick = { handle(item) })    }}``` ## Theming ### Material 3 Dynamic Theming ```kotlin@Composablefun 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.