关于 compose-expert
Compose Multiplatform Expert 是一项技能,帮助规划和实现 Android 与 Desktop 共用的 Compose UI。内容涵盖组件共享、状态管理、主题、图标和重组优化,并说明如何区分平台专属实现。
主要功能
- 01共享 Composable 设计
- 02状态提升与管理模式
- 03重组优化
- 04Material3 主题设置
- 05自定义 ImageVector 图标
- 06Lazy 列表性能指导
适用场景
- 01创建可复用的跨平台组件
- 02判断 UI 的共享位置
- 03重构平台专属 UI
- 04改进 Compose 状态与渲染
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vitorpamplona/ amethyst --skill compose-expert安装后在 Claude Code 中输入/compose-expert确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/icon-assets.mdrich-text-parsing.mdshared-composables-catalog.mdstate-patterns.mdscripts/find-composables.sh详细介绍
Compose Multiplatform 专家
用于在 Android 和 Desktop 之间共享可组合项的视觉 UI 模式。
何时使用此技能
- 创建或重构共享 UI 组件
- 决定是在
commonMain中共享 UI,还是保留为平台专用 - 构建自定义 ImageVector 图标(robohash 模式)
- 状态管理:remember、derivedStateOf、produceState
- 重组优化:通过视觉方式使用 @Stable/@Immutable
- Material3 主题与样式
- 性能:惰性列表、图像加载
委派给其他技能:
- 导航结构 →
android-expert、desktop-expert - Kotlin 状态模式(StateFlow、密封类)→
kotlin-expert - 构建配置 →
gradle-expert
理念:默认共享
默认使用 commonsUI/commonMain(共享可组合项位于 :commonsUI 中,这是共享层中 Compose 的部分;无界面状态/ViewModel 保留在 :commons 中),除非平台专家另有建议。
始终共享
- UI 组件:按钮、卡片、列表、对话框、输入框
- 状态可视化:加载、空、错误状态
- 自定义图标:ImageVector 资源(robohash、自定义路径)
- 主题工具:颜色计算、样式辅助工具
- Material3 组件:任何使用 Material 基元的 UI
屏幕和导航也要共享。 Android 也可运行在笔记本电脑上,而新的
Desktop 应用会渲染相同的 UI,因此屏幕、导航宿主和底部栏/侧边栏/
永久抽屉都属于 commonsUI。它们通过 ScreenLayoutSpec / LocalScreenLayout
适配窗口,而不是按平台区分。请参阅
commons/plans/2026-09-27-one-ui-android-desktop.md。
保留为平台专用
- 入口点:Android
Activity/Service、DesktopWindow、托盘、菜单栏 - 系统集成:文件选择器、通知、分享面板、相机、media3、WebView
- 共享 UI 中的平台专用叶节点:通过 expect/actual 或由适配层填充的插槽实现
决策框架
- 是否仅使用 Material3 基元? → 在
commonMain中共享 - 是否需要平台系统 API? → 平台专用
- 是否为不含导航的纯视觉组件? → 在
commonMain中共享 - 是否需要平台 UX 模式? → 咨询
android-expert或desktop-expert
如不确定,默认选择共享——之后拆分比合并更容易。
共享可组合项的结构
结构
@Composable
fun SharedComponent(
// State parameters (read-only)
data: DataClass,
isLoading: Boolean,
// Event parameters (write-only)
onAction: () -> Unit,
// Visual parameters
modifier: Modifier = Modifier,
// Optional customization
colors: ComponentColors = ComponentDefaults.colors()
) {
// Implementation
}
模式:状态向下传递,事件向上传递
- modifier 上方的参数 = 必需状态/事件
modifier参数 = 布局控制- modifier 下方的参数 = 可选自定义项
示例: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)
}
}
// Shared constants for consistency
val ActionButtonShape = RoundedCornerShape(20.dp)
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
为什么这适用于所有平台:
- Material3 基元(OutlinedButton、Text)
- 不使用平台 API
- 可通过参数配置
- 使用共享常量确保样式一致
状态管理模式
remember - 在重组期间缓存
@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...")
}
}
}
视觉模式:切换按钮 → 状态变化 → UI 展开/折叠 适用于:简单 UI 状态(开关、计数器、文本输入)
derivedStateOf - 优化频繁变化
@Composable
fun ScrollToTopButton(listState: LazyListState) {
// Only recomposes when showButton changes, not every scroll pixel
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
视觉模式:滚动位置(0、1、2……)→ 布尔值(显示/隐藏)→ 按钮可见性 适用于:输入频繁变化,但派生结果很少变化 性能:避免在每次滚动事件时都触发重组
produceState - 将异步操作转换为 Compose 状态
@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!!)
}
}
视觉模式:异步操作 → 状态更新 → UI 反映变化 适用于:将 Flow、LiveData、回调转换为 Compose 状态 生命周期:可组合项离开组合时,协程会被取消
Kotlin 专用状态模式(StateFlow、密封类)请参阅 kotlin-expert。
状态提升
将状态上移,使可组合项可复用:
// ❌ Stateful - hard to test, can't control externally
@Composable
fun BadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ Stateless - reusable, testable
@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)
}
}
原则:状态向上,事件向下
- 状态:
query: String(只读参数) - 事件:
onQueryChange: (String) -> Unit(回调参数)
重组优化
@Immutable 的视觉用法
在传递给可组合项的数据类上使用 @Immutable:
@Immutable
data class UserProfile(val name: String, val avatar: String)
@Composable
fun ProfileCard(profile: UserProfile) {
// Only recomposes when profile instance changes
Row {
RobohashImage(robot = profile.avatar)
Text(profile.name, style = MaterialTheme.typography.titleMedium)
}
}
视觉效果:当父项使用相同的数据重新组合时,阻止重新组合
模式:将参数数据类标记为 @Immutable
注意:有关 @Immutable 的 Kotlin 语言详情,请参阅 kotlin-expert
稳定参数
// ✅ Stable - won't trigger recomposition unless colors instance changes
@Composable
fun ThemedCard(
content: String,
colors: CardColors = CardDefaults.colors(),
modifier: Modifier = Modifier
) {
Card(colors = colors, modifier = modifier) {
Text(content)
}
}
有关 @Stable 注解的详情,请参阅 kotlin-expert。
Material3 主题
所有共享的可组合项都使用 Material3,以保持一致性:
@Composable
fun ThemedComponent() {
val bg = MaterialTheme.colorScheme.background
val fg = MaterialTheme.colorScheme.onBackground
val primary = MaterialTheme.colorScheme.primary
Column(
modifier = Modifier.background(bg)
) {
Text(
"Title",
style = MaterialTheme.typography.headlineMedium,
color = fg
)
Button(
onClick = { /* ... */ },
colors = ButtonDefaults.buttonColors(containerColor = primary)
) {
Text("Action")
}
}
}
原则:
- 颜色:
MaterialTheme.colorScheme.* - 字体排版:
MaterialTheme.typography.* - 形状:
MaterialTheme.shapes.*
主题检测
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
return (background.red + background.green + background.blue) / 3 > 0.5f
}
@Composable
fun ThemedIcon() {
val isDark = !isLightTheme()
val tint = if (isDark) Color.White else Color.Black
Icon(Icons.Default.Face, null, tint = tint)
}
自定义图标:ImageVector 模式
Amethyst 使用 ImageVector 实现多平台图标。
roboBuilder DSL
fun roboBuilder(block: Builder.() -> Unit): ImageVector {
return ImageVector.Builder(
name = "Robohash",
defaultWidth = 300.dp,
defaultHeight = 300.dp,
viewportWidth = 300f,
viewportHeight = 300f
).apply(block).build()
}
构建图标
fun customIcon(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
builder.addPath(pathData3, fill = Black, fillAlpha = 0.2f)
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
lineToRelative(16.0f, 16.0f)
close()
}
@Composable
fun CustomIcon() {
Image(
painter = rememberVectorPainter(
roboBuilder {
customIcon(SolidColor(Color.Blue), this)
}
),
contentDescription = "Custom icon"
)
}
为什么使用 ImageVector?
- 纯 Kotlin,无需 XML
- 适用于 Android、Desktop 和 iOS
- 由 GPU 加速
- 类型安全
缓存模式
object CustomIcons {
private val cache = mutableMapOf<String, ImageVector>()
fun get(key: String): ImageVector {
return cache.getOrPut(key) {
buildIcon(key)
}
}
}
@Composable
fun CachedIcon(key: String) {
Image(imageVector = CustomIcons.get(key), contentDescription = null)
}
有关图标模式的详情,请参阅 references/icon-assets.md。
常见视觉模式
状态可视化
@Composable
fun DataScreen(uiState: UiState) {
when (uiState) {
is UiState.Loading -> LoadingState("Loading...")
is UiState.Empty -> EmptyState(
title = "No data",
onRefresh = { /* refresh */ }
)
is UiState.Error -> ErrorState(
message = uiState.message,
onRetry = { /* retry */ }
)
is UiState.Success -> ContentList(uiState.items)
}
}
组件(均位于 commonsUI/commonMain):
LoadingState- 进度指示器 + 消息EmptyState- 空状态消息 + 可选的刷新按钮ErrorState- 错误消息 + 可选的重试按钮
Relay 状态(Amethyst 模式)
@Composable
fun RelayStatusIndicator(connectedCount: Int) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
tint = statusColor,
modifier = Modifier.size(16.dp)
)
Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
视觉映射:
- 0 个 relay → 红色 + X 图标
- 1-2 个 relay → 黄色 + 勾选图标
- 3 个及以上 relay → 绿色 + 勾选图标
占位符模式
@Composable
fun PlaceholderScreen(
title: String,
description: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(title, style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
Text(description, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Specific implementations
@Composable
fun SearchPlaceholder() = PlaceholderScreen(
title = "Search",
description = "Search for users, notes, and hashtags."
)
模式:通用可组合项 + 预设文本的专用包装器
性能
避免不必要的重新组合
// ❌ Bad - recomposes on every scroll
@Composable
fun BadButton(scrollState: ScrollState) {
if (scrollState.value > 100) {
Button(onClick = {}) { Text("Top") }
}
}
// ✅ Good - only recomposes when visibility changes
@Composable
fun GoodButton(scrollState: ScrollState) {
val show by remember { derivedStateOf { scrollState.value > 100 } }
if (show) {
Button(onClick = {}) { Text("Top") }
}
}
延迟列表
@Composable
fun FeedList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
FeedItem(item)
}
}
}
关键原则:使用 key 参数确保项目身份稳定
捆绑资源
- references/shared-composables-catalog.md - 共享 UI 组件的完整目录
- references/state-patterns.md - 状态管理模式及视觉示例
- references/icon-assets.md - 自定义 ImageVector 图标模式
- references/rich-text-parsing.md -
RichTextParser、UrlParser、GalleryParser、Patterns、MediaContentModels;NIP-92 imeta 丰富化 - scripts/find-composables.sh - 查找代码库中的所有 @Composable 函数
快速参考
| 任务 | 模式 | 位置 |
|---|---|---|
| 可复用 UI | 状态提升 | commonsUI/commonMain |
| 简单状态 | remember { mutableStateOf() } | Composable 作用域 |
| 派生状态 | derivedStateOf { } | remember 代码块 |
| 异步 → 状态 | produceState { } | Composable 函数 |
| 自定义图标 | roboBuilder + PathData | commonsUI/icons |
| 加载/错误 | LoadingState, ErrorState | commonsUI/ui/components |
| 主题颜色 | MaterialTheme.colorScheme | 任意 @Composable |
| 导航 | 委托给平台专家 | amethyst/, desktopApp/ |
常见工作流
创建共享组件
- 从
commonsUI/src/commonMain/kotlin/.../ui/components/开始 - 仅使用 Material3 基元
- 提升状态(数据使用参数,事件使用回调)
- 添加 modifier 参数
- 使用 MaterialTheme 设置颜色/字体排版
- 在 Android 和 Desktop 上测试
转换现有组件
- 阅读
amethyst/或desktopApp/中的当前实现 - 识别纯视觉逻辑(不使用平台 API)
- 在
commonsUI/commonMain中创建组件并提升状态 - 将平台实现替换为共享组件
- 如有需要,保留平台专用包装器
自定义图标
- 从设计工具导出 SVG
- 使用 Android Studio 将其转换为 PathData
- 使用 roboBuilder 创建图标函数
- 如果是动态生成的图标,则添加缓存
- 包装在 @Composable 中以便使用
导航
导航外壳是共享的(ScreenLayoutSpec 会根据窗口大小选择底部栏、导航栏或永久抽屉),并正迁移到带有 AppNavigation 的 commonsUI。平台专属部分仅委托给:
- Activity / intent 相关逻辑 →
android-expert - 窗口、托盘、菜单栏、多窗口 →
desktop-expert - 今日的旧版
desktopApp侧边栏外壳 →desktop-expert(待替换)
相关技能
- kotlin-expert - Kotlin 语言方面(@Immutable 详情、StateFlow、密封类)
- android-expert - Android 导航、平台 API
- desktop-expert - 桌面导航、窗口管理、操作系统特定内容
- kotlin-coroutines - 异步模式、Flow 集成