PluginBench
Skill
Pass
Audit score 90

compose-multiplatform-patterns

affaan-m/everything-claude-code

State management, navigation, theming, and performance patterns for Compose Multiplatform and Jetpack Compose projects.

What is compose-multiplatform-patterns?

Patterns for building shared UI across Android, iOS, Desktop, and Web using Compose Multiplatform and Jetpack Compose. Covers ViewModel-based state management with StateFlow, type-safe navigation, composable design principles, platform-specific UI via expect/actual, and recomposition optimization techniques.

  • Implement single-state ViewModel pattern with StateFlow for predictable UI state
  • Use event sink pattern for complex screens with sealed interfaces instead of multiple callbacks
  • Build type-safe navigation with @Serializable routes in Compose Navigation 2.8+
  • Design reusable composables with slot-based APIs and proper modifier ordering
  • Optimize recomposition with @Stable/@Immutable types, key(), and derivedStateOf()
  • Handle platform-specific UI in KMP projects using expect/actual composables

How to install compose-multiplatform-patterns

npx skills add https://github.com/affaan-m/everything-claude-code --skill compose-multiplatform-patterns
Claude Code
Cursor
Windsurf
Cline

How to use compose-multiplatform-patterns

  1. 1.Define screen state as a single data class and expose it via StateFlow in your ViewModel
  2. 2.Collect state in composables using collectAsStateWithLifecycle() for lifecycle-aware updates
  3. 3.Use sealed interfaces for event handling instead of multiple callback lambdas
  4. 4.Define navigation routes as @Serializable objects and build NavHost with type-safe composable() calls
  5. 5.Mark stable data classes with @Immutable or @Stable to enable recomposition skipping
  6. 6.Use key() in LazyColumn/LazyRow and remember {} to avoid unnecessary allocations
  7. 7.Apply Material 3 theming with dynamic color support for Android 12+
  8. 8.Use expect/actual in commonMain for platform-specific composables in KMP projects

Use cases

Good for
  • Building a multi-platform app with shared Compose UI across Android, iOS, Desktop, and Web
  • Managing complex screen state with multiple user interactions and async operations
  • Implementing navigation with type safety and dialog/bottom sheet overlays
  • Creating a design system with reusable, slot-based composable components
  • Optimizing performance in lists with thousands of items or frequent recompositions
Who it's for
  • Android developers building Jetpack Compose UIs
  • Kotlin Multiplatform (KMP) developers sharing UI code across platforms
  • UI/UX engineers designing composable-based design systems
  • Performance-focused developers optimizing recomposition and rendering
  • Teams adopting type-safe navigation and unidirectional data flow

compose-multiplatform-patterns FAQ

Should I use mutableStateOf in ViewModels?

No. Use MutableStateFlow with collectAsStateWithLifecycle() instead — it's safer for lifecycle management and survives configuration changes.

How do I pass callbacks to deeply nested composables?

Avoid passing NavController deep into composables. Instead, pass simple lambda callbacks and let the parent composable handle navigation logic.

Where should I put heavy computations?

Move heavy computations out of @Composable functions. Use ViewModel methods or wrap them in remember {} blocks to avoid re-running on every recomposition.

What's the difference between event sink and multiple callbacks?

Event sink uses a single sealed interface (e.g., ItemListEvent) instead of many callback lambdas. This reduces parameter count and makes intent clearer.

How do I optimize lists with many items?

Use stable key() for each item, mark data classes @Immutable, and use derivedStateOf() for derived state. Avoid creating new objects in composable parameters.

Full instructions (SKILL.md)

Source of truth, from affaan-m/everything-claude-code.


name: compose-multiplatform-patterns description: Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI. 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:

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

@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:

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:

@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:

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:

@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:

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

// 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:

@Immutable
data class ItemUiModel(
    val id: String,
    val title: String,
    val description: String,
    val progress: Float
)

Use key() and Lazy Lists Correctly

LazyColumn {
    items(
        items = items,
        key = { it.id }  // Stable keys enable item reuse and animations
    ) { item ->
        ItemRow(item = item)
    }
}

Defer Reads with derivedStateOf

val listState = rememberLazyListState()
val showScrollToTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}

Avoid Allocations in Recomposition

// 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

@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.