mcpField
C
compose-expert

compose-expert

github.com/vitorpamplona设计与 UI1.6k1.4k 次安装今天更新
代码仓库

关于 compose-expert

Compose Multiplatform Expert 是一项技能,帮助规划和实现 Android 与 Desktop 共用的 Compose UI。内容涵盖组件共享、状态管理、主题、图标和重组优化,并说明如何区分平台专属实现。

主要功能

  1. 01共享 Composable 设计
  2. 02状态提升与管理模式
  3. 03重组优化
  4. 04Material3 主题设置
  5. 05自定义 ImageVector 图标
  6. 06Lazy 列表性能指导

适用场景

  1. 01创建可复用的跨平台组件
  2. 02判断 UI 的共享位置
  3. 03重构平台专属 UI
  4. 04改进 Compose 状态与渲染

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vitorpamplona/amethyst --skill compose-expert

安装后在 Claude Code 中输入/compose-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
icon-assets.md
rich-text-parsing.md
shared-composables-catalog.md
state-patterns.md
scripts/
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、Desktop Window、托盘、菜单栏
  • 系统集成:文件选择器、通知、分享面板、相机、media3、WebView
  • 共享 UI 中的平台专用叶节点:通过 expect/actual 或由适配层填充的插槽实现
决策框架
  1. 是否仅使用 Material3 基元? → 在 commonMain 中共享
  2. 是否需要平台系统 API? → 平台专用
  3. 是否为不含导航的纯视觉组件? → 在 commonMain 中共享
  4. 是否需要平台 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 + PathDatacommonsUI/icons
加载/错误LoadingState, ErrorStatecommonsUI/ui/components
主题颜色MaterialTheme.colorScheme任意 @Composable
导航委托给平台专家amethyst/, desktopApp/

常见工作流

创建共享组件
  1. 从 commonsUI/src/commonMain/kotlin/.../ui/components/ 开始
  2. 仅使用 Material3 基元
  3. 提升状态(数据使用参数,事件使用回调)
  4. 添加 modifier 参数
  5. 使用 MaterialTheme 设置颜色/字体排版
  6. 在 Android 和 Desktop 上测试
转换现有组件
  1. 阅读 amethyst/ 或 desktopApp/ 中的当前实现
  2. 识别纯视觉逻辑(不使用平台 API)
  3. 在 commonsUI/commonMain 中创建组件并提升状态
  4. 将平台实现替换为共享组件
  5. 如有需要,保留平台专用包装器
自定义图标
  1. 从设计工具导出 SVG
  2. 使用 Android Studio 将其转换为 PathData
  3. 使用 roboBuilder 创建图标函数
  4. 如果是动态生成的图标,则添加缓存
  5. 包装在 @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 集成

评论

支持 Markdown