affaan-m/ECC

compose-multiplatform-patterns

Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.

67Collecting
See how to use itView GitHub source
npx skills add https://github.com/affaan-m/ECC --skill "skills/compose-multiplatform-patterns"
Automated source guideDesignDeep source

Source checked Jul 28, 2026·Refresh due Oct 26, 2026

Reorganized from the pinned upstream SKILL.md

Source-grounded design guide: 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.

npx skills add https://github.com/affaan-m/ECC --skill "skills/compose-multiplatform-patterns"
Check the pinned source

The pinned source contains enough sections and task detail for a source-grounded deep guide; automated content is still not an independent test.

774 source words · 21 usable sections

Design process

Read compose-multiplatform-patterns through these 5 source sections

Sections are extracted automatically from the pinned SKILL.md and link back to the source.

01

When to Activate

Building Compose UI (Jetpack Compose or Compose Multiplatform)

SKILL.md · When to Activate
Building Compose UI (Jetpack Compose or Compose Multiplatform)Managing UI state with ViewModels and Compose stateImplementing navigation in KMP or Android projects
02

State Management

Use a single data class for screen state. Expose it as StateFlow and collect in Compose:

SKILL.md · State Management
Use a single data class for screen state. Expose it as StateFlow and collect in Compose:For complex screens, use a sealed interface for events instead of multiple callback lambdas:
03

ViewModel + Single State Object

Use a single data class for screen state. Expose it as StateFlow and collect in Compose:

SKILL.md · ViewModel + Single State Object
Use a single data class for screen state. Expose it as StateFlow and collect in Compose:
04

Collecting State in Compose

Review the “Collecting State in Compose” section in the pinned source before continuing.

SKILL.md · Collecting State in Compose
Review and apply the “Collecting State in Compose” source section.
05

Event Sink Pattern

For complex screens, use a sealed interface for events instead of multiple callback lambdas:

SKILL.md · Event Sink Pattern
For complex screens, use a sealed interface for events instead of multiple callback lambdas:

SkillSignal prompt templates

Provide the task, context, and acceptance criteria

These prompts were written by SkillSignal from the source structure; they are not upstream text.

Source-grounded prompt

Use for a design task while explicitly checking the source sections.

Use compose-multiplatform-patterns for this design task: [task]. Inputs and constraints: [details]. Work through these pinned SKILL.md sections: “When to Activate”, “State Management”, “ViewModel + Single State Object”, “Collecting State in Compose”, “Event Sink Pattern”. Cite the concrete requirements that shape each step, do not invent capabilities absent from the source, and verify the result against: [acceptance criteria].

Design checklist

Verify each item before delivery

The source section “When to Activate” has been checked.

The source section “State Management” has been checked.

The source section “ViewModel + Single State Object” has been checked.

The source section “Collecting State in Compose” has been checked.

Choose a different workflow

When another Skill is the better fit

FAQ

What does the compose-multiplatform-patterns source document cover?

Patterns for building shared UI across Android, iOS, Desktop, and Web using Compose Multiplatform and Jetpack Compose. Covers state management, navigation, theming, and performance.

How do I install compose-multiplatform-patterns?

The source record exposes this install command: npx skills add https://github.com/affaan-m/ECC --skill "skills/compose-multiplatform-patterns". Inspect the command and pinned source before running it.

Repository stars
234,327
Repository forks
35,711
Quality
67/100
Source repository last pushed

Quality breakdown

Based on traceable docs and repository signals; stars are not treated as quality.

67/100
Documentation23/30
Specificity7/25
Maintenance20/20
Trust signals17/25

Compare before choosing

Related Agent Skills and source variants

These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.

compose-multiplatform-patterns by affaan-m

Use it for engineering and design tasks; the detail page covers purpose, installation, and practical steps.

compose-multiplatform-patterns by affaan-m

Use it for engineering and design tasks; the detail page covers purpose, installation, and practical steps.

ab-testing by coreyhaines31

When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program

churn-prevention by coreyhaines31

When the user wants to reduce churn, build cancellation flows, set up save offers, recover failed payments, or implement retention strategies. Also use when the user mentions 'churn,' 'cancel flow,' 'offboarding,' 'save offer,' 'dunning,' 'failed payment recovery,' 'win-back,' 'retention,' 'exit survey,' 'pause subscription,' 'involuntary churn,' 'people keep canceling,' 'churn rate is too high,' 'how do I keep users,' or 'customers are leaving.' Use this whenever someone is losing subscribers o

design-intelligence by event4u-app

Grounded design brief from the adopted corpus — style, WCAG-checked color tokens, typography, layout pattern, anti-patterns. Use on ui-design-brief or any which-style/palette/font/chart decision.

View original Skill.mdThis page is parsed directly from the repository SKILL.md without editorial rewriting. Collected: Jul 28, 2026 · about 2 min

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.

Source repo
affaan-m/ECC
Skill path
skills/compose-multiplatform-patterns/SKILL.md
Commit SHA
4e973d3eaf92
Repository license
MIT
Data collected