SKILL.md
readonlyread-only
name
compose-multiplatform-patterns
description
Compose Multiplatform 與 Jetpack Compose 在 KMP 專案中的模式——狀態管理、導航、主題、效能與平台特定 UI。
Compose Multiplatform 模式
使用 Compose Multiplatform 與 Jetpack Compose 在 Android、iOS、桌面與網頁上建立共用 UI 的模式。涵蓋狀態管理、導航、主題與效能。
啟用時機
- 建構 Compose UI(Jetpack Compose 或 Compose Multiplatform)
- 使用 ViewModel 與 Compose 狀態管理 UI 狀態
- 在 KMP 或 Android 專案中實作導航
- 設計可重複使用的 composable 與設計系統
- 最佳化重組與渲染效能
狀態管理
ViewModel + 單一狀態物件
使用單一 data class 表示畫面狀態。以 StateFlow 暴露,並在 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) } }
)
}
}
}
在 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
) {
// 無狀態 composable — 易於預覽與測試
}
事件匯集模式
對於複雜畫面,使用 sealed interface 處理事件,而非多個回呼 lambda:
sealed interface ItemListEvent {
data class Search(val query: String) : ItemListEvent
data class Delete(val itemId: String) : ItemListEvent
data object Refresh : ItemListEvent
}
// 在 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)
}
}
// 在 Composable 中 — 單一 lambda 取代多個
ItemListContent(
state = state,
onEvent = viewModel::onEvent
)
導航
型別安全導航(Compose Navigation 2.8+)
將路由定義為 @Serializable 物件:
@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() 與覆蓋層模式,而非命令式的 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 設計
基於插槽的 API
使用插槽參數設計 composable 以增加彈性:
@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 順序
Modifier 順序很重要——依此順序套用:
Text(
text = "Hello",
modifier = Modifier
.padding(16.dp) // 1. 佈局(padding, size)
.clip(RoundedCornerShape(8.dp)) // 2. 形狀
.background(Color.White) // 3. 繪製(background, border)
.clickable { } // 4. 互動
)
KMP 平台特定 UI
expect/actual 用於平台 Composable
// 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 透過 UIKit 互通或 Info.plist 處理
}
效能
穩定型別以實現可跳過重組
當所有屬性都穩定時,將類別標記為 @Stable 或 @Immutable:
@Immutable
data class ItemUiModel(
val id: String,
val title: String,
val description: String,
val progress: Float
)
正確使用 key() 與 Lazy List
LazyColumn {
items(
items = items,
key = { it.id } // 穩定的 key 可啟用項目重用與動畫
) { item ->
ItemRow(item = item)
}
}
使用 derivedStateOf 延遲讀取
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
避免在重組中分配物件
// 不好——每次重組都產生新的 lambda 與 list
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
// 好——為每個項目設定 key,使回呼保持與正確的 row 關聯
val activeItems = remember(items) { items.filter { it.isActive } }
activeItems.forEach { item ->
key(item.id) {
ActiveItem(item, onClick = { handle(item) })
}
}
主題
Material 3 動態主題
@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)
}
應避免的反模式
- 在 ViewModel 中使用
mutableStateOf,而MutableStateFlow搭配collectAsStateWithLifecycle對生命週期更安全 - 將
NavController深入傳遞給 composable——改傳遞 lambda 回呼 - 在
@Composable函式中進行大量計算——移到 ViewModel 或remember {} - 使用
LaunchedEffect(Unit)替代 ViewModel 初始化——在某些設定下會因設定變更而重新執行 - 在 composable 參數中建立新的物件實例——導致不必要的重組
參考資料
請參閱技能:android-clean-architecture 了解模組結構與分層。
請參閱技能:kotlin-coroutines-flows 了解協程與 Flow 模式。






