compose-expert について
Compose Multiplatform Expertは、AndroidとDesktopで共有するCompose UIの設計・実装を支援するスキルです。共有範囲の判断から状態管理、テーマ、アイコン、再コンポジションの最適化までを扱います。
主な機能
- 01共有Composableの設計
- 02状態のホイスティングと管理
- 03再コンポジションの最適化
- 04Material3のテーマ設定
- 05カスタムImageVectorアイコン
- 06Lazyリストの性能指針
活用シーン
- 01再利用可能なクロスプラットフォーム部品の作成
- 02UIの共有範囲の判断
- 03プラットフォーム固有UIのリファクタリング
- 04Composeの状態管理と描画の改善
インストール
コマンドをコピーしてターミナルで実行します。
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 コンポーネントの作成またはリファクタリング
- 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 が埋めるスロットとして扱います
判断基準
- 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、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 + 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/インテントの連携 →
android-expert - ウィンドウ、トレイ、メニューバー、マルチウィンドウ →
desktop-expert - 現在のレガシーな
desktopAppサイドバーシェル →desktop-expert(置き換え予定)
関連スキル
- kotlin-expert - Kotlin 言語の側面(@Immutable の詳細、StateFlow、sealed classes)
- android-expert - Android のナビゲーション、プラットフォーム API
- desktop-expert - デスクトップのナビゲーション、ウィンドウ管理、OS 固有の事項
- kotlin-coroutines - 非同期パターン、Flow の統合