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
- Material3 theming and styling
- Performance: lazy lists, image loading
Delegate to other skills:
- Navigation structure β
android-expert, desktop-expert
- Kotlin state patterns (StateFlow, sealed classes) β
kotlin-expert
- Build configuration β
gradle-expert
Philosophy: Share by Default
Default to commons/commonMain unless platform experts indicate otherwise.
Always Share
- UI components: Buttons, cards, lists, dialogs, inputs
- State visualization: Loading, empty, error states
- Custom icons: ImageVector assets (robohash, custom paths)
- Theme utilities: Color calculations, style helpers
- Material3 components: Any UI using Material primitives
Keep Platform-Specific
- Navigation structure: Bottom nav (Android) vs Sidebar (Desktop)
- Screen layouts: Platform-specific scaffolding
- System integrations: File pickers, notifications, share sheets
- Platform UX: Gestures, keyboard shortcuts, window management
Decision Framework
- Uses only Material3 primitives? β Share in
commonMain
- Requires platform system APIs? β Platform-specific
- Pure visual component without navigation? β Share in
commonMain
- Needs platform UX patterns? β Ask
android-expert or desktop-expert
If uncertain, default to sharing - easier to split later than merge.
Shared Composable Anatomy
Structure
@Composable
fun SharedComponent(
data: DataClass,
isLoading: Boolean,
onAction: () -> Unit,
modifier: Modifier = Modifier,
colors: ComponentColors = ComponentDefaults.colors()
) {
}
Pattern: State down, events up
- Parameters above modifier = required state/events
modifier parameter = layout control
- Parameters below modifier = optional customization
Example: AddButton
@Composable
fun AddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Add",
enabled: Boolean = true
) {
OutlinedButton(
modifier = modifier,
enabled = enabled,
onClick = onClick,
shape = ActionButtonShape,
contentPadding = ActionButtonPadding
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
val ActionButtonShape = RoundedCornerShape(20.dp)
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
Why this works on all platforms:
- Material3 primitives (OutlinedButton, Text)
- No platform APIs
- Configurable through parameters
- Consistent styling via shared constants
State Management Patterns
remember - Cache Across Recompositions
@Composable
fun ExpandableCard() {
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
@Composable
fun ScrollToTopButton(listState: LazyListState) {
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { }) {
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
@Composable
fun LoadUserProfile(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = repository.fetchUser(userId)
}
}
@Composable
fun ProfileScreen(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:
@Composable
fun BadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
@Composable
fun GoodSearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier
)
}
@Composable
fun SearchScreen() {
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:
@I