Communitygithub.com

Compose 多平台模式

Compose Multiplatform 与 Jetpack Compose 在 KMP 项目中的模式——涵盖状态管理、导航、主题、性能及平台特定 UI。在搭建 Compose 或 Jetpack Compose UI、处理状态、导航或主题时使用。

Compose 多平台模式 是什么?

该技能文档提供了在 Kotlin Multiplatform (KMP) 项目中使用 Compose Multiplatform 和 Jetpack Compose 构建共享 UI 的完整模式集合。涵盖了从 ViewModel 加单一状态对象的状态管理、事件收集模式,到使用 `@Serializable` 路由的类型安全导航、对话框与底部导航栏的声明式管理。在可组合项设计方面,文档强调了基于槽位的 API 以提升灵活性、修饰符的正确排序以及平台特定 UI 的 `expect/actual` 机制。性能优化部分深入讲解稳定类型(`@Stable` / `@Immutable`)以支持可跳过重组、懒加载列表中的 `key()` 使用、`derivedStateOf` 延迟读取以及避免重组中的对象分配。此外,还介绍了 Material 3 动态主题方案以及诸如在 ViewModel 中直接使用 `mutableStateOf` 或深层传递 NavController 等常见反模式。这套模式适用于所有 Compose Multiplatform 目标平台(Android、iOS、桌面及 Web),能够帮助开发者构建一致、高性能且易于维护的 UI 代码。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/compose-multiplatform-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Compose 多平台模式 是做什么的?

适用于使用 Compose Multiplatform 和 Jetpack Compose 在 Android、iOS、桌面和 Web 上构建共享 UI 的模式。涵盖状态管理、导航、主题和性能。

何时激活

  • 构建 Compose UI(Jetpack Compose 或 Compose Multiplatform)
  • 使用 ViewModel 和 Compose 状态管理 UI 状态
  • 在 KMP 或 Android 项目中实现导航
  • 设计可复用的 Composable 和设计系统
  • 优化重组和渲染性能

状态管理

ViewModel + 单一状态对象

使用单一数据类表示屏幕状态。将其公开为 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 — 易于预览和测试
}

事件汇集模式

对于复杂屏幕,使用密封接口处理事件而非多个回调 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() 和叠加模式,而非命令式的显示/隐藏:

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)
        }
    }
}

修饰符顺序

修饰符顺序很重要——请按以下顺序应用:

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() 和懒加载列表

LazyColumn {
    items(
        items = items,
        key = { it.id }  // 稳定的键可支持项复用和动画
    ) { item ->
        ItemRow(item = item)
    }
}

使用 derivedStateOf 延迟读取

val listState = rememberLazyListState()
val showScrollToTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}

避免在重组中进行对象分配

// 错误 — 每次重组都创建新的 lambda 和列表
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }

// 正确 — 为每个项设置键,使回调保持与正确的行绑定
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 模式。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能