Install any skill in seconds. Free to start, no credit card required.
Get Started Free →KMPプロジェクト向けのCompose MultiplatformおよびJetpack Composeパターン — 状態管理、ナビゲーション、テーマ設定、パフォーマンス、プラットフォーム固有のUI。
.claude/skills/affaan-m-compose-multiplatform-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 67% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 92% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 60% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 79% | 0% |
使用 Compose Multiplatform 和 Jetpack Compose 构建跨 Android、iOS、桌面和 Web 的共享 UI 的模式。涵盖状态管理、导航、主题和性能。
使用单个数据类表示屏幕状态。将其暴露为 StateFlow 并在 Compose 中收集:
kotlindata 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) } } ) } } }
kotlin@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 }
对于复杂屏幕,使用密封接口表示事件,而非多个回调 lambda:
kotlinsealed 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 )
将路由定义为 @Serializable 对象:
kotlin@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() 和覆盖层模式,而非命令式的显示/隐藏:
kotlinNavHost(navController, startDestination = HomeRoute) { composable<HomeRoute> { /* ... */ } dialog<ConfirmDeleteRoute> { backStackEntry -> val route = backStackEntry.toRoute<ConfirmDeleteRoute>() ConfirmDeleteDialog( itemId = route.itemId, onConfirm = { navController.popBackStack() }, onDismiss = { navController.popBackStack() } ) } }
使用槽位参数设计可组合项以获得灵活性:
kotlin@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) } } }
修饰符顺序很重要 —— 按此顺序应用:
kotlinText( 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 )
kotlin// 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 }
当所有属性都稳定时,将类标记为 @Stable 或 @Immutable:
kotlin@Immutable data class ItemUiModel( val id: String, val title: String, val description: String, val progress: Float )
key() 和惰性列表kotlinLazyColumn { items( items = items, key = { it.id } // Stable keys enable item reuse and animations ) { item -> ItemRow(item = item) } }
derivedStateOf 延迟读取kotlinval listState = rememberLazyListState() val showScrollToTop by remember { derivedStateOf { listState.firstVisibleItemIndex > 5 } }
kotlin// 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) }) } }
kotlin@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) }
mutableStateOf,而 MutableStateFlow 配合 collectAsStateWithLifecycle 对生命周期更安全NavController 深入传递到可组合项中 —— 应传递 lambda 回调@Composable 函数中进行繁重计算 —— 应移至 ViewModel 或 remember {}LaunchedEffect(Unit) 作为 ViewModel 初始化的替代 —— 在某些设置中,它会在配置更改时重新运行查看技能:android-clean-architecture 了解模块结构和分层。 查看技能:kotlin-coroutines-flows 了解协程和 Flow 模式。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 16,822 | 15,782 | -6% | 1 | 1 | 0% | 3,235 | 5,393 | +67% | 0 | 0 | — |
case-02 | pass→pass | 9,770 | 7,145 | -27% | 1 | 1 | 0% | 1,696 | 3,250 | +92% | 0 | 0 | — |
case-03 | pass→pass | 12,955 | 11,046 | -15% | 1 | 1 | 0% | 2,558 | 4,093 | +60% | 0 | 0 | — |
case-04 | pass→pass | 10,318 | 8,040 | -22% | 1 | 1 | 0% | 2,082 | 3,729 | +79% | 0 | 0 | — |
case-05 | pass→pass | 11,989 | 10,503 | -12% | 1 | 1 | 0% | 2,306 | 3,997 | +73% | 0 | 0 | — |
case-06 | pass→pass | 12,520 | 12,280 | -2% | 1 | 1 | 0% | 2,331 | 4,357 | +87% | 0 | 0 | — |
case-07 | fail→pass | 14,231 | 10,499 | -26% | 1 | 1 | 0% | 2,451 | 3,869 | +58% | 0 | 0 | — |
case-08 | pass→pass | 10,959 | 8,486 | -23% | 1 | 1 | 0% | 2,374 | 3,812 | +61% | 0 | 0 | — |
case-09 | pass→pass | 12,083 | 11,600 | -4% | 1 | 1 | 0% | 2,246 | 4,212 | +88% | 0 | 0 | — |
case-10 | pass→pass | 10,005 | 7,251 | -28% | 1 | 1 | 0% | 1,439 | 3,227 | +124% | 0 | 0 | — |
case-11 | pass→pass | 11,822 | 22,025 | +86% | 1 | 1 | 0% | 2,027 | 3,622 | +79% | 0 | 0 | — |
case-12 | pass→pass | 14,258 | 11,697 | -18% | 1 | 1 | 0% | 2,342 | 4,132 | +76% | 0 | 0 | — |
case-13 | pass→pass | 14,227 | 13,958 | -2% | 1 | 1 | 0% | 2,535 | 4,650 | +83% | 0 | 0 | — |
case-14 | pass→pass | 12,773 | 10,746 | -16% | 1 | 1 | 0% | 2,227 | 3,881 | +74% | 0 | 0 | — |
case-15 | pass→pass | 12,503 | 13,787 | +10% | 1 | 1 | 0% | 2,185 | 4,456 | +104% | 0 | 0 | — |
case-16 | pass→pass | 9,678 | 8,507 | -12% | 1 | 1 | 0% | 1,738 | 3,744 | +115% | 0 | 0 | — |
case-17 | pass→pass | 11,957 | 13,734 | +15% | 1 | 1 | 0% | 1,985 | 4,236 | +113% | 0 | 0 | — |
case-18 | pass→pass | 11,365 | 8,843 | -22% | 1 | 1 | 0% | 1,886 | 3,567 | +89% | 0 | 0 | — |
case-19 | pass→pass | 14,836 | 13,865 | -7% | 1 | 1 | 0% | 2,706 | 4,885 | +81% | 0 | 0 | — |
case-20 | pass→pass | 5,834 | 9,680 | +66% | 1 | 1 | 0% | 1,209 | 4,013 | +232% | 0 | 0 | — |
case-21 | pass→pass | 15,888 | 16,083 | +1% | 1 | 1 | 0% | 3,187 | 5,413 | +70% | 0 | 0 | — |
case-22 | pass→pass | 10,970 | 7,083 | -35% | 1 | 1 | 0% | 1,849 | 3,416 | +85% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.