Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
Compose Multiplatform Expert
Visual UI patterns for sharing composables across Android and Desktop.
When to Use This Skill
Creating or refactoring shared UI components
Deciding whether to share UI in commonMain or keep platform-specific
Building custom ImageVector icons (robohash pattern)
State management: remember, derivedStateOf, produceState
Recomposition optimization: visual usage of @Stable/@Immutable
@ComposablefunExpandableCard() {
var isExpanded by remember { mutableStateOf(false) }
Column {
IconButton(onClick = { isExpanded = !isExpanded }) {
Icon(
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Collapse"else"Expand"
)
}
if (isExpanded) {
Text("Expanded content...")
}
}
}
Visual pattern: Toggle button → state changes → UI expands/collapses
Use for: Simple UI state (toggles, counters, text input)
derivedStateOf - Optimize Frequent Changes
@ComposablefunScrollToTopButton(listState: LazyListState) {
// Only recomposes when showButton changes, not every scroll pixelval showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
Visual pattern: Scroll position (0, 1, 2...) → boolean (show/hide) → Button visibility
Use for: Input changes frequently, derived result changes rarely
Performance: Prevents recomposition on every scroll event
produceState - Async to Compose State
@ComposablefunLoadUserProfile(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = repository.fetchUser(userId)
}
}
@ComposablefunProfileScreen(userId: String) {
val user by LoadUserProfile(userId)
when (user) {
null -> LoadingState("Loading profile...")
else -> ProfileCard(user!!)
}
}
Visual pattern: Async operation → state updates → UI reflects changes
Use for: Convert Flow, LiveData, callbacks into Compose state
Lifecycle: Coroutine cancelled when composable leaves composition
For Kotlin-specific state patterns (StateFlow, sealed classes), see kotlin-expert.
State Hoisting
Move state up to make composables reusable:
// ❌ Stateful - hard to test, can't control externally@ComposablefunBadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ Stateless - reusable, testable@ComposablefunGoodSearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier
)
}
@ComposablefunSearchScreen() {
var query by remember { mutableStateOf("") }
Column {
GoodSearchBar(query = query, onQueryChange = { query = it })
SearchResults(query = query)
}
}
Principle: State up, events down
State: query: String (read-only parameter)
Events: onQueryChange: (String) -> Unit (callback parameter)
Recomposition Optimization
Visual Usage of @Immutable
Use @Immutable on data classes passed to composables:
@ImmutabledataclassUserProfile(val name: String, val avatar: String)
@ComposablefunProfileCard(profile: UserProfile) {
// Only recomposes when profile instance changes
Row {
RobohashImage(robot = profile.avatar)
Text(profile.name, style = MaterialTheme.typography.titleMedium)
}
}
Visual effect: Prevents recomposition when parent recomposes with same data
Pattern: Mark parameter data classes as @Immutable
Note: For Kotlin language details on @Immutable, see kotlin-expert