Compose Multiplatform Muster
Muster für die gemeinsame UI über Android, iOS, Desktop und Web hinweg mit Compose Multiplatform und Jetpack Compose. Behandelt State-Management, Navigation, Theming und Leistungsoptimierung.
Wann aktivieren
- Erstellen von Compose UI (Jetpack Compose oder Compose Multiplatform)
- Verwalten des UI-Status mit ViewModels und Compose-Zuständen
- Implementieren der Navigation in KMP- oder Android-Projekten
- Entwerfen wiederverwendbarer Composables und Designsysteme
- Optimieren von Recomposition und Rendering-Leistung
State-Management
ViewModel + Einzelstatusobjekt
Verwenden Sie eine einzige Datenklasse für den Bildschirmstatus. Stellen Sie sie als StateFlow bereit und sammeln Sie sie 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) } }
)
}
}
}
Status in Compose sammeln
@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
) {
// Zustandsloses Composable – einfach zu previewen und testen
}
Event-Sink-Muster
Verwenden Sie für komplexe Bildschirme ein versiegeltes Interface für Ereignisse anstelle mehrerer Callback-Lambdas:
sealed interface ItemListEvent {
data class Search(val query: String) : ItemListEvent
data class Delete(val itemId: String) : ItemListEvent
data object Refresh : ItemListEvent
}
// Im 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)
}
}
// Im Composable – ein einziges Lambda statt vieler
ItemListContent(
state = state,
onEvent = viewModel::onEvent
)
Navigation
Typsichere Navigation (Compose Navigation 2.8+)
Definieren Sie Routen als @Serializable-Objekte:
@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- und Bottom-Sheet-Navigation
Verwenden Sie dialog() und Overlay-Muster anstelle von imperativem Ein-/Ausblenden:
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-basierte APIs
Entwerfen Sie Composables mit Slot-Parametern für Flexibilität:
@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-Reihenfolge
Die Reihenfolge der Modifier ist wichtig – wenden Sie sie in dieser Reihenfolge an:
Text(
text = "Hallo",
modifier = Modifier
.padding(16.dp) // 1. Layout (Abstand, Größe)
.clip(RoundedCornerShape(8.dp)) // 2. Form
.background(Color.White) // 3. Zeichnen (Hintergrund, Rahmen)
.clickable { } // 4. Interaktion
)
KMP-Plattformspezifische UI
expect/actual für Plattform-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 handhabt dies über UIKit-Interop oder Info.plist
}
Leistungsoptimierung
Stabile Typen für überspringbare Recomposition
Markieren Sie Klassen mit @Stable oder @Immutable, wenn alle Eigenschaften stabil sind:
@Immutable
data class ItemUiModel(
val id: String,
val title: String,
val description: String,
val progress: Float
)
key() und Lazy-Listen korrekt verwenden
LazyColumn {
items(
items = items,
key = { it.id } // Stabile Schlüssel ermöglichen Wiederverwendung und Animationen
) { item ->
ItemRow(item = item)
}
}
Lesevorgänge mit derivedStateOf verzögern
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
Allokationen bei Recomposition vermeiden
// SCHLECHT – neues Lambda und neue Liste bei jeder Recomposition
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
// GUT – jedes Element mit key versehen, sodass Callbacks an der richtigen Zeile haften
val activeItems = remember(items) { items.filter { it.isActive } }
activeItems.forEach { item ->
key(item.id) {
ActiveItem(item, onClick = { handle(item) })
}
}
Theming
Material 3 Dynamisches 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, die vermieden werden sollten
- Verwenden von
mutableStateOfin ViewModels, wennMutableStateFlowmitcollectAsStateWithLifecyclesicherer für den Lebenszyklus ist - Übergeben von
NavControllertief in Composables – stattdessen Lambda-Callbacks übergeben - Schwere Berechnungen innerhalb von
@Composable-Funktionen – in ViewModel oderremember {}verschieben - Verwenden von
LaunchedEffect(Unit)als Ersatz für ViewModel-Init – es wird bei Konfigurationsänderungen in manchen Setups erneut ausgeführt - Erstellen neuer Objektinstanzen in Composable-Parametern – verursacht unnötige Recomposition
Referenzen
Siehe Skill: android-clean-architecture für Modulstruktur und Schichtung.
Siehe Skill: kotlin-coroutines-flows für Coroutine- und Flow-Muster.