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. 02UIの共有範囲の判断
  3. 03プラットフォーム固有UIのリファクタリング
  4. 04Composeの状態管理と描画の改善

インストール

コマンドをコピーしてターミナルで実行します。

$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 コンポーネントの作成またはリファクタリング
  • UI を commonMain で共有するか、プラットフォーム固有にするかの判断
  • カスタム ImageVector アイコンの作成(robohash パターン)
  • 状態管理:remember、derivedStateOf、produceState
  • 再コンポジションの最適化:@Stable/@Immutable の視覚的な使用
  • Material3 のテーマ設定とスタイリング
  • パフォーマンス:遅延リスト、画像読み込み

ほかのスキルに委任する項目:

  • ナビゲーション構造 → android-expert、desktop-expert
  • Kotlin の状態パターン(StateFlow、sealed classes) → kotlin-expert
  • ビルド設定 → gradle-expert

方針:デフォルトで共有する

デフォルトでは commonsUI/commonMain を使用します(共有コンポーザブルは共有レイヤーの Compose 部分である :commonsUI に配置し、ヘッドレスな状態/ViewModel は :commons に配置します)。ただし、プラットフォームのエキスパートが別の方法を推奨する場合を除きます。

常に共有するもの
  • UI コンポーネント:ボタン、カード、リスト、ダイアログ、入力欄
  • 状態の表示:読み込み中、空、エラーの状態
  • カスタムアイコン:ImageVector アセット(robohash、カスタムパス)
  • テーマユーティリティ:色の計算、スタイルヘルパー
  • Material3 コンポーネント:Material プリミティブを使用するすべての UI

画面とナビゲーションも共有します。**Android はノート PC でも動作し、新しい 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 の背後に配置するか、shim が埋めるスロットとして扱います
判断基準
  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、sealed classes)については、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 - エラーメッセージと任意の再試行ボタン
リレーの状態(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 件 → 赤色 + X アイコン
  • リレー 1~2 件 → 黄色 + チェックアイコン
  • リレー 3 件以上 → 緑色 + チェックアイコン
プレースホルダーパターン
@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/インテントの連携 → android-expert
  • ウィンドウ、トレイ、メニューバー、マルチウィンドウ → desktop-expert
  • 現在のレガシーな desktopApp サイドバーシェル → desktop-expert(置き換え予定)

関連スキル

  • kotlin-expert - Kotlin 言語の側面(@Immutable の詳細、StateFlow、sealed classes)
  • android-expert - Android のナビゲーション、プラットフォーム API
  • desktop-expert - デスクトップのナビゲーション、ウィンドウ管理、OS 固有の事項
  • kotlin-coroutines - 非同期パターン、Flow の統合

コメント