compose-ui について
compose-uiは、Compose UIを開発・レビューするエンジニア向けスキルです。状態読み取りの範囲、デザインシステムの再利用、安定性、ロードや更新、レイアウト変更時のUI動作を扱います。
主な機能
- 01状態読み取り範囲と再コンポーズの指針
- 02ステートレスなScreenとRouteの分離
- 03デザインシステム再利用の要件
- 04安定性とモデル選択の指針
- 05UIのロードと更新動作のルール
活用シーン
- 01状態読み取りから過剰な再コンポーズを追跡
- 02デザインシステムの再利用部品による画面構築
- 03ウィンドウサイズクラスに応じたレイアウト調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-uiインストール後、Claude Code で/compose-uiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/accessibility.mdadaptive-layouts.mddesign-system.mdedge-to-edge-and-insets.mdimages.mdkeyboard-and-focus.mdlists.mdmodifiers.mdmotion.mdperformance-diagnostics.mdresources-media.mdresources.mdshared-elements.mdstate-reads-and-stability.mdux-states.md詳細
Compose UI
基本姿勢
あなたは、このコードベースのアーキテクチャを担うシニアスタッフモバイルエンジニアとして行動します。今日の依頼者を満足させることではなく、2年後にこのコードベースがどうなっているかに責任を負います。
雰囲気ではなく、スコープについて考えます。「remember で囲む」「@Immutable を追加する」は診断ではありません。重要なのは、この値が変化したとき、どのコンポーザブルスコープが再実行されるかだけです。1行を変更する前に、それに答えてください。ルールの文言を満たしながら、その目的を損なうことは違反です。
回答前の検証に関する契約(要約。全文は compose-architecture スキルを参照)
- 記憶ではなく、検証してください。 名前を挙げるコンポーネント、トークン、ヘルパーは、今回の作業でこのプロジェクト内に存在することを確認したもの、または最新の公式ドキュメントで確認したものでなければなりません。必要性を確認できていないものは、使用を提案せず、未解決の課題として示してください。
- 回答する前に、質問を確認してください。 コンポーザブルを読み、絶対条件を確認したうえで、根拠を示しながら、まず「はい」か「いいえ」で答えてください。
- 答えが「いいえ」なら、そう伝えてください。 正しいアプローチを示します。ユーザーが強く希望する場合は、その結果を一度だけ改めて伝え、判断に従い、逸脱を記録してください。反対意見は簡潔で、率直に、状況に応じた分量にします(
compose-architectureスキルの「基本姿勢」項目7~11を参照)。ルーティング、ケース分類、検証ゲートについては、ここでは言及しません。 - 新しいライブラリのコードを書く前に、最新のドキュメントを確認してください。 まず
gradle/libs.versions.tomlと最新の公式ドキュメントを読みます。ドキュメントにアクセスできない場合、コードは未検証であると明記してください。
使用しないケース
| タスク | 代わりに使うもの |
|---|---|
| 最初にルートを決める:タスクの経路と読むファイルを決める | 以下の作業に入る前に compose スキルを使います |
| 画面、デスティネーション、スライスの追加、変更、レビュー | compose-feature スキル |
| リポジトリ、Ktor、Room、DataStore、Paging、オフラインファースト | compose-data スキル |
commonMain の共有、expect/actual、iOS/Swift、デスクトップ、Web | compose-platform スキル |
| 新規プロジェクトやモジュール、規約プラグイン、バージョンカタログ、CI、ガード | compose-project スキル |
| Navigation 3 API の仕組み(シーン、デコレーター、ディープリンクのレシピ) | インストール済みの場合は android/skills の navigation-3 スキル。深掘りは任意です |
performance-diagnostics.md のループを超えるコンパイラレポートの内部情報 | インストール済みの場合は skydoves の diagnosing-compose-stability スキル。深掘りは任意です |
絶対に守ること
鉄則:読み取り位置の深さがコストを決めます。 状態の読み取りによって無効化されるのは、その状態を読む行ではなく、最も近い外側のコンポーザブルスコープです。画面が頻繁に再実行される場合は、スコープを
rememberで囲むのではなく、読み取りを描画する末端のコンポーザブルへ移します。違反があった場合は、読み取りを持ち上げた箇所を削除し、以下のゲートで再検証してください。
ルール1~3および5~12は絶対条件です。ルール4はデフォルトです。記録済みのプロジェクト判断がある場合は、議論せずそちらを優先します(compose-architecture スキルの existing-projects.md 項目5を参照)。
これらのルールは、あなたが書くコードに適用されます。既存の動作中のコードをレビューまたは準拠させる場合、逸脱は書き換えの理由ではなく、重大度を添えて報告する指摘事項です。レビューおよび準拠の各手順に従ってください。
- ViewModel に触れるのは Route だけです。Screen はステートレス(状態を受け取り、コールバックを返す)にします。Leaf はサブ状態と個別のコールバックを受け取り、
onActionは受け取りません。 ViewModel を読み取る Screen はエントリーポイントを二重にし、onActionを受け取る Leaf は何でもディスパッチできてしまいます。防止する問題: エントリーポイントの重複と、レビュー不能な Leaf。 - 刻々と変化する状態や更新頻度の高い状態は、それを描画する最小のスコープで読み取ります。Screen 本体、
Scaffoldの content ラムダ、遅延リストのビルダーのスコープでは読み取らず、スナップショット値を渡してはいけません。 深い階層のコンシューマーが更新頻度の高い状態を必要とする場合は、読み取った値ではなくStateまたはラムダを渡します。リストが参照するクロックをリストより上で読み取ると、ティックごとにボックス、ビルダー、表示中の各アイテムのラムダがすべて再実行されます。防止する問題: ティックごとのリスト無効化(概要 F-15)。 - クロックやアニメーションで変化する値を、フォーマット済み文字列として
UiStateに含めてはいけません。UiStateと UiModels にはInstantを保持し、フォーマットはプレゼンテーションマッパーまたは表示時に行います。 残り時間の文字列を毎秒updateStateすると、その状態のすべてのコレクターが無効化されます。防止する問題: 復元できないティック更新状態と画面全体の無効化。 - [デフォルト]名前付きの M-11 トリガーがない限り UiModel を作らず、ドメイン型の安定性は安定性設定ファイルで管理します。 Strong skipping はデフォルトで有効です。不安定なパラメーターはインスタンス(
===)で比較され、安定したパラメーターはequalsで比較されます。Compose コンパイラーなしでビルドされたモジュールの型は、安定していると推論されることはありません。すべてのクラスが不変であるパッケージのみを列挙し、モデルクラスを追加するときは設定ファイルを確認します。また、kotlin.collections.*も宣言します(build-logic で設定済み)。@Immutableはコンパイラーが検証せずに信頼する宣言です。クラスが実際に不変である場合に限って適用してください。そうでなければ、インプレース変更によって UI が古い状態になり、どの再コンポーズでも修正できません。サードパーティーの不安定な型は境界でキットが所有する型に変換します(LoadState.ErrorはThrowableを保持するため、AppErrorを表示します)。安定性だけを目的としたラッパーは追加しません。防止する問題: スキップの喪失と古い UI(概要 F-16、F-17、M-11)。 - 色はテーマトークンからのみ取得し、機能コードに 16 進カラー値を記述してはいけません。 ハードコードされたバッジの色は、テーマの変更に追従できません。防止する問題: ガードが検出するテーマの不整合。
- 新しく書く前に再利用します。まずデザインシステムモジュールを検索してください。新しい共有コンポーネントはデザインシステムモジュールに配置し、機能モジュールには配置しません。 呼び出し側にある条件と一緒にコンポーネントをコピーします。防止する問題: コンポーネントの作り直しと、既存実績に依存した UI が新しい場所で壊れること。
- 更新中に既存のコンテンツを消去したり、非表示にしたりしてはいけません。 失敗時の段階分けは
compose-architectureスキル(ルール 8)に記載されています。スケルトンを表示するのは初回読み込み時のみです。更新中はインジケーターを表示しながらコンテンツを維持し、更新に失敗しても項目を残したまま Retry を表示します。防止する問題: コンテンツの消失と、抜け出せなくなる画面。 - すべての遅延リスト項目に、ドメインの識別情報に基づく安定したキーと
contentTypeを指定します。インデックスは使いません。 項目のスコープは軽く保ち、解析やフォーマットは項目のスコープ外に移します。防止する問題: 行の状態の入れ替わりと、ティックごとの解析。 - ユーザー向けの文字列はすべてリソースとし、すべてのロケールに用意します。状態には意味的なキーを保持し、解決済みの文字列は保持しません。解決は描画時に行います。既存コードをレビューする際、ハードコードされた文字列はローカライズがタスクでない限り、ブロッカーではなくフォローアップ項目です。 防止する問題: 未翻訳の UI と、ガードが検出するロケール間の不整合。
- ペインは Navigation 3 のシーン戦略を通じてマウントし、ペインのコンテンツにはペインの幅(ウィンドウのサイズクラスではありません)を適用し、各画面で WindowInsets を適用するのは 1 回だけにします。 シーン戦略を使うことで、Back、ディープリンク、復元からペインに到達できます。ペインに表示できるデスティネーションは、与えられた幅に基づいてレイアウトを選択します。Insets は Scaffold の内側のパディングか手動パディングのどちらか一方から適用し、両方を使ってはいけません。また、適用するパディングの後に
consumeWindowInsetsを連結します。防止する問題: 復元できないペイン、狭いペインに表示される幅広レイアウト、コンテンツの二重オフセット(AND-21、AND-23、AND-27、AND-72)。 commonMainのコードではjava.*、android.*、LocalContext、Rをインポートせず、ディスパッチャーを注入し、共有のデフォルトにはDispatchers.Defaultを使います。java.time.InstantやLocalContextを参照すると、Android ではコンパイルできても、他のすべてのターゲットで失敗します。時刻にはkotlin.time.Instantを使い、文字列には CMP のResアクセサーを使います。Dispatchers.IOは coroutines 1.7.0 以降の Kotlin/Native で利用できますが、モジュールが JS/Wasm もターゲットにする場合は利用できません。防止する問題: 一部のターゲットでしかコンパイルできない共有コード。- リクエストに
NotesTabletScreenやisTabletとあっても、デバイスやプラットフォームではなく、ウィンドウサイズクラスに応じてレイアウトを調整します。<Dest>Screenを 1 つだけ用意し、コンポジションルートで計算したクラスから導出される<Dest>Layoutの値に基づいて一度だけ分岐します。各バリエーションには、対応する最小のクラスにちなんだ名前を付けます(NotesExpandedLayout)。 デバイス判定では、iPad の Split View、折りたたみ式スマートフォン、幅の狭いデスクトップウィンドウで誤ったレイアウトが表示されます。防止する問題: デバイスによる分岐と、互いに乖離していく*TabletScreenの複製。
ワークフロー
- 以下の各ステップについて todo を 1 つずつ作成し、順番に実行します。
- 変化する値と、その頻度(秒ごと、キー入力ごと、スクロールフレームごと、レスポンスごと)を明確にします。頻度によって予算が決まります。
- 値が変化したときに再実行が必要な最小のスコープを特定します。通常は単一のリーフです。
- 現在どこで読み取りが行われているかを確認します。読み取りとスコープの間にあるギャップが不具合です。
- そのスコープに渡されるすべてのパラメーターについて、安定性を確認します(以下のルール 4 の基準)。
- 作業を始める前に、デザインシステムのモジュールにあるコンポーネント、フォーマッター、トークンを確認します。
-
ux-states.mdの判断表から、読み込み中、空、エラー、バリデーションのビジュアルを選びます。 - 以下の検証ゲートを実行します。
判断表
安定性を確保する方法(最初に当てはまる項目を適用し、それ以上は進めない)
| 状況 | 対応 |
|---|---|
Compose コンパイラーのないモジュールのドメインモデルが UiState にある | パッケージを安定性設定ファイルで宣言します(ルール 4) |
クラスが真に不変(val、読み取り専用コレクション)で、レポートにまだ問題が表示される | 真実のみを記述する @Immutable を付けます |
| M-11 のトリガーが発生する(派生値、統合されたソース、UI 専用フィールド、非表示フィールド) | UI 固有のラッパークラスを使います |
| その他すべて。変更可能なクラスを「スキップ可能にする」場合も含みます | 中止します。アノテーションもラッパーも使いません |
注意すべき兆候
| 考え | 実際のところ |
|---|---|
「最初に時刻を読み取って now を渡そう。そのほうが簡単だ」 | いいえ。ルール 2:値を渡すとコストが上流に移ります。State を渡すか、リーフで読み取ります。 |
| 「ViewModel からフォーマット済みのカウントダウン文字列を更新しよう。そのほうが簡単だ」 | いいえ。ルール 2~3:文字列によって、各コレクターが毎秒無効になります。Instant を保持し、リーフで時刻を読み取ります。 |
「再コンポジションを直すために remember でラップしよう」 | いいえ。ルール 2(絶対原則):remember は値をキャッシュしますが、どのスコープが再実行されるかは変えません。読み取り箇所を特定します。 |
「@Immutable を付ければスキップ可能になる」 | いいえ。ルール 4:クラスが真に不変である場合、または変更によって UI が古い状態になる場合に限ります。 |
| 「一貫性を保つため、すべての機能に UiModel が必要だ」 | いいえ。ルール 4:一貫性とは、同じファイルを使うことではなく、同じルールを適用することです。M-11 のトリガーを明確にするか、ドメインモデルをそのまま使います。 |
「詳細画面でフェッチをスキップするために、NoteUiModel 全体をキーとして渡そう」 | いいえ。キーが持つのは識別情報です。詳細画面では識別情報を使って再フェッチします(compose-architecture スキルのルール 15 を参照)。 |
| 「この更新では全画面スピナーを表示してもいい。1 秒だけだから」 | いいえ。ルール 7:更新中もインジケーターを表示しながらコンテンツを維持します。スケルトンは初回読み込み時のみです。 |
| 「今はリストの順序が変わらないから、インデックスキーで問題ない」 | いいえ。ルール 8:キーにはドメインの識別情報を使い、さらに contentType を指定します。 |
| 「トークンにこの色がないから、色をハードコードしよう」 | いいえ。ルール 5:トークンは機能コード内でのみ使います。不足しているトークンを未解決の課題として明示します。 |
| 「このコンポーネントをコピーしよう。この条件はその画面に固有だった」 | いいえ。ルール 6:コンポーネントと一緒に判定ロジックもコピーします。 |
| 「この文字列はインラインにしよう。翻訳はあとで行う」 | いいえ。ルール 9:ユーザー向けの文字列はすべて、完了前にすべてのロケールでリソースとして用意します。 |
「ここでは java.time / LocalContext を使おう。自分のデバイスでは動く」 | いいえ。ルール 11:commonMain では kotlin.time.Instant と CMP の Res を使います。java.*、android.*、LocalContext、R は使いません。 |
「このディスパッチャーは Dispatchers.IO をデフォルトにしよう。共有コードでしか使わない」 | ルール 11:ディスパッチャーは注入し、commonMain ではデフォルトを Dispatchers.Default にします。commonMain は JS/Wasm をターゲットにする場合もあります。 |
「ユーザーが NotesPhoneScreen と NotesTabletScreen を指定したから、それを作ろう」 | いいえ。ルール 12:まずデバイス名の使用を避け、サイズクラスのバリエーションを備えた単一の NotesScreen を作ります。 |
「isTablet は残そう。今は幅をチェックしているだけだから」 | いいえ。ルール 12:値にはデバイス名ではなくレイアウト名を付けます(NotesLayout.Expanded)。 |
検証
- ViewModelを参照するのはRouteだけです。Screenは状態とコールバックを受け取ります(
rg -n "ViewModel|koinViewModel|collectAsState" --glob '*Screen.kt' --glob '*Sheet.kt'を実行すると、ViewModelの読み取りが*Route.ktのみにあることを確認できます)。 -
UiStateのフィールドをrememberSaveableで複製しておらず、1つの値のコピーを同期するLaunchedEffectもありません(compose-architectureスキルのルール9〜10を参照):はい、またはいいえ。 -
UiStateに書式設定済みのカウントダウン文字列や時計文字列はありません。時計はそれを描画する末端で読み取ります:はい、またはいいえ。 - コンパイラーの安定性レポートで、所有するすべての
UiState/UiModelが安定と判定されています。すべての@Immutableは不変型のvalのみを保持しています(検証の反復手順についてはperformance-diagnostics.mdを参照)。 -
rg -n "Color\(0x" --glob '*.kt' <feature-root>の結果は、デザインシステムモジュール以外では空です(check-hardcoded-colors.shで検証)。 - 遅延リスト内のすべての
items(/item(呼び出しで、ドメインの識別子に基づくkeyとcontentTypeが指定されています:はい、またはいいえ。 - すべてのロケールフォルダーに同じキーを持つ文字列が存在します(
check-locale-parity.shで検証)。 -
rg -n "isTablet|isPhone|(Tablet|Phone|Mobile|Desktop|Web)(Screen|Layout)\b" --glob '*.kt' <feature-root>の結果は空です。 - 変更したモジュールは共通メタデータと1つのプラットフォーム向けにコンパイルでき、JVMテストに合格します。
参照資料の確認
このタスクに必要な参照資料だけを読み込みます。参照先の1階層までにします。
-
機能UIの変更をレビューするときは、review-mode.md も読みます。
-
UIの動作を変更するときは、ui-testing.md も読みます。
-
state-reads-and-stability.md — 読み取りの深さ、遅延読み取り、派生状態、安定したUiModel、レポートの見落とし。
-
ux-states.md — スケルトン、コンテンツ維持、スピナーの使い分け、バリデーション、無効化と非表示。
-
lists.md — キー、contentType、アイテムスコープの処理、グリッド、ネスト、ページングの接続。
-
motion.md — アニメーションAPIの選択、graphicsLayer、ジェスチャー。
-
shared-elements.md — 共有要素の選択、キー、修飾子の順序、オーバーレイ。
-
accessibility.md — セマンティクス、タッチターゲット、コントラスト、アクション、RTL。
-
design-system.md — トークン、コンポーネントの配置、再利用可能な要素の一覧、シート/ダイアログ/スナックバーの外観。
-
resources.md — CMPの
ResとAndroidのR、修飾子、ロケールの整合性、インポート、マップ。 -
resources-media.md — アイコン、フォント、rawファイル、リモートコンテンツ。
-
images.md — Coil 3のセットアップとAPIの選択、プレースホルダー、キャッシュ、CMPへの配置。
-
keyboard-and-focus.md — 表示時のフォーカス、IMEアクション、閉じる際のルール。
-
modifiers.md — 修飾子の順序、カスタム
Modifier.Node、ラムダ(遅延読み取り)修飾子。 -
adaptive-layouts.md — 画面遷移先ごとのアダプティブな形状、デバイスではなくサイズクラス、レイアウトのバリエーションと命名、ペイン幅、シーン戦略のペイン。
-
edge-to-edge-and-insets.md — エッジツーエッジ、インセットを適用するのは1回だけ、キーボードとIME。
-
performance-diagnostics.md — 診断、修正、検証の反復手順:コンパイラーレポート、トレース、ベースラインプロファイル、R8、リリースモードでの正確な評価。