compose-screens-footguns について
KotlinとCompose Multiplatformの開発者向けに、レイアウト、ナビゲーション、操作の落とし穴を扱うスキルです。症状別の項目で、画面実装の問題に対するパターンと検証手順を説明します。
主な機能
- 01トピック別の短い参考ファイル32件
- 02レイアウトとナビゲーションのパターン
- 03操作上の問題と原因の説明
- 04プロジェクト向け検証コマンド
- 05症状別インデックスと相互参照
活用シーン
- 01サイズ変更されたウィンドウへの画面適応
- 02ナビゲーションタブの欠落や不整合の調査
- 03スクロール画面のオーバーレイとインセットの確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ maxrave-dev/ kotlin-footguns --skill compose-screens-footgunsインストール後、Claude Code で/compose-screens-footgunsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/collapsing-parallax-toolbar.mdcommit-a-text-field-on-an-explicit-action.mdcontrol-range-must-cover-stored-values.mdcustom-modal-sheet-family.mdcustom-thin-media-slider.mddesktop-mini-player-window.mdedit-a-shape-as-a-shape.mdempty-state-must-keep-its-navigation.mdevent-not-state-edge-for-one-shot-effects.mdexpandable-and-linkified-text.mdfloating-overlay-reserves-its-own-strip.mdfullscreen-video-gesture-overlay.mdhidden-setting-does-not-clear-the-stored-value.mdlayered-video-backdrop-pager.mdlazy-item-grouping-beats-arrangement-gap.mdlazy-list-drag-reorder.mdlazy-scroll-helper-kit.mdnav-tab-registration-drift.mdnowplaying-pager-no-feedback-loop.mdprogress-indicator-as-scrubber.mdresponsive-gate-size-not-platform.mdscoped-composable-shadows-the-top-level-one.mdscreen-shell-content-split.mdselection-mode-state-holder.mdself-hiding-child-cannot-hide-its-slot.mdsettings-value-round-tripped-through-its-label.mdstacked-bars-double-consume-window-insets.mdstory-reel-auto-pager.mdswipe-action-list-row.mdtype-safe-nav-graph-organization.md詳細
Compose 画面の落とし穴:レイアウト、ナビゲーション、操作
本番環境の Kotlin および Compose Multiplatform アプリから収集した 32 の落とし穴です。各項目は references/ 以下の短いファイル 1 つにまとめられています。各ファイルには、適切なパターン、落とし穴(実際に問題が起きる具体的なケースと、その理由)、そしてユーザー自身のツリーに対して実行するコマンドを示す「検証方法」セクションがあります。
このインデックスの使い方。 これから書くコード、または調査中の症状に照らして、以下の項目を確認してください。それぞれの項目には、トピックと、該当する項目を参照すべき症状が記載されています。コードや修正を提案する前に、該当する可能性のあるファイルをすべて Read ツールで読んでください。どのファイルも 150 行未満です。「検証方法」のコマンドにソースプロジェクトのパスが記載されている場合は、ユーザーのツリー内の対応するパスに置き換えてください。
相互参照。 この一覧に記載されていないファイル内のバッククォートで囲まれた落とし穴名は、関連するスキル領域にあります。../*/references/<name>.md を glob して開いてください。
画面、ナビゲーション、アダプティブレイアウト
responsive-gate-size-not-platform— アダプティブレイアウトの切り替えはプラットフォームではなく、ウィンドウ自体の幅と高さに基づいて行い、切り替えが制御するすべてのジオメトリ値(フレームサイズ、コンテンツのスケール、スクリーンの高さ)を同時に切り替えます。スマートフォンでは適切なレイアウトが、幅の狭いデスクトップウィンドウやサイズ変更後には崩れる場合、アダプティブフラグを一律の値に固定すると別のプラットフォームの画面が崩れる場合、またはウィンドウが広いとアートワークがページ全体を覆ってしまう場合に使います。nav-tab-registration-drift— トップレベルのタブは、独自のタブリストを保持するすべてのナビゲーション領域(ボトムバー、レール、2つのリストを持つ装飾付きバー)に加え、グラフとそのタブを有効にするフラグにも登録する必要があります。そうしないと、コード上にはタブが存在しても表示されません。同様のずれは、複数のトップアプリバーに表示されるステータスの入口にも起きます。バッジがないと、不具合ではなく「何も実行されていない」と見えてしまいます。タブの序数が位置ではなく識別子である理由、条件付きのタブがユーザーの操作で消える場合に必要なもの、バッジの点にページカラーの縁取りが必要な理由を説明します。新しいタブやバッジがある領域では表示されるのに別の領域では表示されない場合、並べ替え後に選択状態が間違ったタブを強調表示する場合、またはタブが1つ増えた途端にタブバーがあふれる場合に使います。type-safe-nav-graph-organization— 型安全な Navigation-Compose グラフを、遷移先が増えても読みやすく保てるように構成します。エリアごとにまとめた、ファイルごとに1つのシリアライズ可能なルートオブジェクト、グラフビルダーの拡張関数としてのエリアグラフ、ホストにまとめた1つの遷移セット、各エントリー内で画面をラップして適用するルートごとのテーマ設定を扱います。ナビゲーションファイルが数百行に増えた場合、ルート引数にカスタム型が必要になった場合、または特に理由もなく遷移やテーマが遷移先ごとに異なる場合に使います。collapsing-parallax-toolbar— 1つのボックス内にある5つの兄弟要素から、折りたたみ式ヘッダーを構築します。うち4つは同じスクロール状態を共有し、残る1つはブール値で制御します。アートワークはグラフィックスレイヤーでスクロールの半分の速度で移動し、タイトルは2区間の曲線に沿って固定バー内へ補間され、導出した反転ポイントでフローティングの戻るボタンを通常のトップバーに切り替えます。パララックスヘッダーがスクロール中に揺れたり再計測されたりする場合、折りたたまれるタイトルが意図した経路からずれる場合、またはウィンドウのサイズ変更後に固定バーが間違ったスクロール位置で表示される場合に使います。fullscreen-video-gesture-overlay— 全画面ビデオ画面のジェスチャーレイヤーとクロームレイヤーを扱います。画面のどこをタップしてもコントロールを切り替えられ、左右どちら側でもダブルタップでシークでき、操作のたびに自動非表示タイマーが延長されます。また、ピクチャーインピクチャー(PiP)時には両方のレイヤーを取り除くガードを設けます。シングルタップが画面の半分でしか機能しない場合、シークバーをドラッグ中にコントロールが消える場合、シークのつまみが指の下で元に戻る場合、ダブルタップでリップルが表示されない、または消えない場合、小さなフローティングウィンドウ内のビデオにアプリのクロームが重なったままの場合に使います。nowplaying-pager-no-feedback-loop— 再生中のアートワークページャーがプレーヤーに追従しながらプレーヤーも操作でき、互いの書き込みがループしないようにします。スクロール中フラグはドラッグだけでなくプログラムによるアニメーションにも適用され、シークはページが確定したときにのみ送信され、ページ差分の判定は UI ランタイムの外部に置いた純粋関数で行います。スワイプすると元のページに戻る場合、1回のスワイプで2曲飛ばしてしまう場合、高速スワイプ後にページャーがプレーヤーに追従しなくなる場合、戻るスワイプで1つ前の曲に戻らず現在の曲が再スタートする場合、またはシャッフル中の大きなスワイプで間違った曲に移動する場合に使います。layered-video-backdrop-pager— スワイプ可能な再生中ページを構築します。カラーバックドロップ、全面に表示するループ動画または画像、中央に配置する正方形のカバーをすべて1つのページャーにまとめ、各ページの色には実際にそのページで描画されたビットマップの色を使い、スクリーンをページ全体に広げます。スワイプ時に背景とカバーがずれて動く場合、スワイプ中に別の曲の動画が隣のページにはみ出す場合、バックドロップの色が別の曲のものになったりページ間で黒く点滅したりする場合、動画の下端がコントロール越しに見える場合、またはタップオーバーレイがページャーのドラッグを妨げる場合に使います。desktop-mini-player-window— 再生コントロール用に、常に最前面に表示される枠なしのデスクトップウィンドウをもう1つ作ります。その存在はコンポジションの外側にある1つのブール値で保持し、メインウィンドウと同じ状態オブジェクトを使います。ポインターの絶対座標を基準に独自のドラッグ処理を実装し、デバイスピクセル単位でネイティブの最小サイズを設定し、ストアに過剰な書き込みをせずにウィンドウの位置とサイズを永続化します。ドラッグ中に小さなウィンドウがずれたり揺れたりする場合、角を越えてサイズを変更できない場合、コンテンツより小さく縮む場合、メインウィンドウを閉じた瞬間に消える場合、表示されずに開く場合、またはその状態がメインウィンドウと一致しない場合に使います。screen-shell-content-split— 1つの画面を、外観をまたぐすべての関心事を管理するシェルと、描画のみを行うコンテンツレイヤーに分け、状態のスナップショットとアクションのバッグという2つのホルダーでつなぎます。これにより、2つ目の外観を追加する際の変更は、引数リストの編集ではなく分岐1つで済みます。ホルダーが安定しているが不変ではない理由、シェルに残すべきもの、残してはいけないものを扱います。画面に2つ目のビジュアルスタイルを追加する場合、スタイルの切り替えでスクロール位置やアートワークのページがリセットされる場合、外観の追加に50個の引数を持つシグネチャの編集が必要になる場合、または「共通」ヘルパーにスタイルごとのifが必要になり始めた場合に使います。variant-layout-math-stays-in-the-variant— 1つの画面にある2つの外観で、それぞれ画面にぴったり収まる計測(固定ブロックを計測し、残りを均等な間隔に分け、最小値を下回らないようにする)が必要な場合は、共通化せず、外観ごとに計算を持たせます。ブロックに必要なエフェクトキー、実際に描画される比率と一致させる必要がある見えないスペーサー、アニメーション可能なスペーサーを扱います。ヒーロー要素の上下の間隔に前の曲のサイズが残る場合、折り返し位置で終わるはずのコンテンツがはみ出す、または空白の帯ができる場合、あるいは2つの画面にある「同じ」レイアウト計算を1つのヘルパーにまとめる前に使います。empty-state-must-keep-its-navigation— 空の状態メッセージで、データがあるときのヘッダーを置き換えても、そのヘッダーが持っていたコントロールは削除しません。コントロールは、それを持っていた分岐でのみ再提供し、読み込み中の分岐は空状態の分岐より先に判定し、テキスト1行のためにアートワークの高さを確保しないようにします。ユーザーがデータのない期間、フィルター、検索結果に行き着いて戻れなくなる場合、再読み込みのたびに空メッセージが一瞬表示される場合、または同じコントロールが2回表示される場合に使います。stacked-bars-double-consume-window-insets— インセットの消費はコンポーザブルの子孫には伝わりますが、兄弟要素には伝わりません。そのため、1つのカラムにインセット対応バーを2つ積み重ねると、それぞれがシステムバーの領域を確保し、バー1つ分の高さの空白ができます。フレームワークのデフォルトを使ってバーのwindowInsetsをパラメーター化する方法、インセットを消費する箇所を1か所に決める方法、同じコンポーネントのオーバーレイ呼び出し箇所でもデフォルトを維持すべき理由を扱います。2つのバーの間に空白ができる場合、画面の特定のモードでのみ表示される場合、またはインセットをゼロにした後、横向きでバーの先頭アイコンが切れる場合に使います。floating-overlay-reserves-its-own-strip— スクロールするカラム上にフローティング表示されるコントロールは、計測に参加しない兄弟要素です。そのため、カラム側でコントロールが覆う領域を明示的に確保する必要があります。先頭のスペーサーを置く方法と、2つのスペーサーで中央揃えのタイトルを空いた領域の上でも中央に保つヘッダー行を置く方法を扱います。領域の定数に含めてよいものと含めてはいけないもの、スクロールする各分岐にそれぞれ領域確保が必要な理由、ページをスクロールした後もコントロールに手が届くようにするにはフローティング表示が必要な理由を説明します。フローティングの戻るボタンがコンテンツの最初の行に重なる場合、中央揃えのタイトルがボタン半分ぶんずれる場合、または長いページでコントロールが手の届かない位置までスクロールしてしまう場合に使います。
Components and interaction
lazy-list-drag-reorder— 遅延コンポーズされる Compose リスト向けの、ドラッグによる並べ替えを完全に管理する状態ホルダーです。ポインターオフセットの累積、表示中の範囲に対する移動先インデックスの計算、持ち上げアニメーションと組み込みのアイテム配置アニメーションを重ねてはならない理由、呼び出し側が差分を使って動かす端での自動スクロール、データレイヤーとのドロップ時コミット契約を扱います。並べ替えを実装するとき、ドラッグした行が元の位置に戻る、リストがその下で再レイアウトされる際にちらつく、ユーザーがキャンセルした移動をコミットする、または行ではなくリストがスクロールするときに利用してください。swipe-action-list-row— タップ、長押しによる選択、横スワイプによるアクションという3つのジェスチャーを同時に受け付けるリスト行と、タップの意味を変えるモードフラグです。スワイプ検出器が現在のモードとコンポジション時に取得したモードのどちらを認識するかを決めるpointerInputキー、レイアウトフェーズでの行の移動、コミットしきい値の固定、反対方向のドラッグを親に任せる方法を扱います。複数選択を開始した後も行がスワイプされる、行がたまたま再コンポーズされるまで選択が機能しない、またはスワイプが下層のページャーやリストと競合するときに利用してください。selection-mode-state-holder— Compose リストでの複数選択用の小さな@Stableホルダーです。リスト位置ではなく安定した ID をキーにし、すべての変更を1つのプライベートメソッドに集約することで選択数の上限を回避できないようにします。また、2種類のジェスチャーを異なる意味にする、切り替え/クリア/すべて選択のセマンティクスも扱います。リストの一括操作を実装するとき、並べ替えやページ読み込み後に選択対象の行がずれるとき、または行のタップには上限が適用されるのに「すべて選択」には適用されないときに利用してください。expandable-and-linkified-text— 文字数ではなく測定したオーバーフローに基づく「もっと見る/閉じる」操作で長文を数行に制限し、同じテキスト内のタイムスタンプや URL をタップ可能にします。「もっと見る」がすでに収まっているテキストに表示される、収まらないテキストでは表示されない、テキストと一緒に切り詰められる、またはリンクをタップするとリンク先に移動せずブロックが展開されるときに利用してください。lazy-scroll-helper-kit— アプリ間で使える、遅延リスト向けの小さなユーティリティ4つです。派生状態としてのスクロール方向、測定前に1フレーム待つアイテムの中央スクロール、アイテムの表示率、表示中の範囲からの検索と、それぞれに潜む落とし穴を扱います。スクロール時に隠れるバーがちらついたり固まったりする、アイテムへのスクロール位置が端になったり何も起きなかったりする、またはビューポートの計算で別のアイテムの値が返るときに利用してください。custom-thin-media-slider— Material3 の Slider にカスタムのトラックとつまみのスロットを指定して作る、バッファー済み再生位置のトラックを背面に備えた細いシークバーです。つまみが末端に張り付くのを防ぐ「独自の尺度ではなく割合を使う」ルールと、ドラッグ中に再生位置の更新が競合しないようにする状態ゲートも扱います。シークバーが位置に関係なく全体表示または空表示になる、ドラッグ中につまみが戻る、細くできない、またはトラックに不要な点や目盛りが表示されるときに利用してください。custom-modal-sheet-family— Material3 の ModalBottomSheet に独自のスタイルを適用します。透明なコンテナと独自のサーフェス、明示的な末尾スペーサーを使ったウィンドウインセットのゼロ化、自作のドラッグハンドル、シートがコンポジションから外れる前に閉じるアニメーションを完了させる、非表示にしてからの破棄を扱います。シートがスライドせずに閉じる、最後の行がナビゲーションバーの下に隠れる、内部のテキストが見えない、またはシート群のスタイルが少しずつばらばらになったときに利用してください。control-range-must-cover-stored-values— スライダーの範囲は、隣接するコントロールの範囲ではなく、保存、インポート、移行された値など、実際に渡される値の分布に基づいて設定してください。範囲外の値が渡されると、読み取り値には別の数値が表示される一方で、つまみはトラックの末端に固定され、最初のタッチで値が黙って書き換えられるためです。スライダー以外の場所から値が渡される可能性のある設定にスライダーを追加するときに利用してください。edit-a-shape-as-a-shape— 編集対象が曲線なら、N個のスライダーではなくドラッグ可能な曲線を描き、画面に遷移させるのではなく設定リストに埋め込みます。ドラッグジェスチャーヘルパーではなく生のポインターループを使い、1回のジェスチャーにつき1回だけコミットする下書きを用意し、ユーザーが配置したハンドルをスムージングで超えないようにします。複数ポイントのエディターを実装するとき、曲線コントロールがタップに反応しない、リリース時に元に戻る、または保存済みの値を消してしまうときに利用してください。progress-indicator-as-scrubber— スライダーではなく、プログレスインジケーターと透明なポインターレイヤーでシークコントロールを作ります。表示部分より高いヒットボックス、祖先のページャーにシーク操作を奪われないように消費するドラッグ、操作中に表示するドラッグ独自の割合、自分で描画するつまみ、レイアウト上の位置を変えずに要素を手前に描く判断を扱います。必要な見た目に相当するスライダーがない、スワイプやシートにシーク操作を奪われる、ドラッグ中にバーが元に戻る、または細いコントロールにタッチできないときに利用してください。scoped-composable-shadows-the-top-level-one— レイアウトスコープ内では、同名のスコープ拡張コンポーザブルがトップレベル版より優先されます。どちらもコンパイルできるため、これは気づきにくく、通常版を意図した呼び出しにスコープ版のデフォルト値とレイアウト動作が適用されます。トップレベル版を強制する完全修飾、スコープ版を強制するthis@Scope.、ネストしたレイアウトの内側でも外側のスコープが適用される理由を扱います。表示/非表示アニメーションで親がフェードインせずに拡大・縮小する、列や行に移動した後でコンポーザブルの動作が変わる、または意図していないオーバーロードが呼び出されるときに利用してください。settings-value-round-tripped-through-its-label— 選択ダイアログで、選ばれたローカライズ済みラベルを保存値に対応付けると、2つのラベルが同じ翻訳になった時点で問題が起きます。書き込みがスキップされるか、エラーなしで別の選択肢に反映されます。テキストではなく ID を保持する方法、見つからない場合に明確に失敗させる方法、ストアとコレクターの初期値の両方にデフォルトを指定することで生じる関連リスクを扱います。ある言語でだけ設定の選択が保存されない、ピッカーが隣の選択肢を書き込む、または画面を開いたときに一瞬だけ間違った種類が表示されるときに利用してください。self-hiding-child-cannot-hide-its-slot— 機能が使えないときに何も描画しないコンポーネントでも、別の場所で囲まれたコンテナは取り除けません。同じ利用可否条件でスロットを制御し、その条件をコンポーネントとともに公開し、スロットのクリック可能な形式とクリック不可の形式を適切に分岐させてください。任意のコントロールがあるはずの場所に空のセルや隙間、余分な区切り線が現れる、グループの角丸の端が間違ったアイテムに付く、またはラッパーが内部のコントロールへのタップを妨げるときに利用してください。event-not-state-edge-for-one-shot-effects— 1回限りの祝福エフェクトは、クリックによって生じた状態ではなく、そのクリックから実行してください。ブール値の偽→真の変化を監視するエフェクトでは、タップと、その少し後に届いたデータを区別できません。そのため、すでにマーク済みのレコードに移動したときも同じ変化が発生します。実行中のエフェクトを管理する@Stableホルダー、タップ時点での意味の判定、以前の値を保持する変数が第2の真実の情報源になる理由、同時に実行されるエフェクトを加算的に扱う方法を扱います。レコードを切り替えているだけなのにエフェクトが再生される、ユーザーがしていない操作を祝福する、または画面に戻った直後の最初の正当なタップが黙って無視されるときに利用してください。weight-fill-false-to-center-a-cluster— 重み付きの子は、内容の測定幅が狭くてもスロット全体を占めるため、隣の要素が画面の端に寄ります。weight(1f, fill = false)を指定すると、使われていない幅が行の配置に戻り、子、隙間、隣の要素をひとまとまりに見せられます。手掛かりが重み付きの子ではなく隣の要素に現れる理由、修正に必要なのが1か所ではなく2か所である理由、重み自体を削除するのとは別のレイアウトになる理由を扱います。コンテンツのサイズによってバーの中央揃えが崩れる、ボタンが画面端に張り付いて隣に穴ができる、または末尾のラベルが対応するテキストから離れているときに利用してください。lazy-item-grouping-beats-arrangement-gap— 遅延リストのspacedBy配置はすべてのアイテム間に適用され、各アイテム自身の端のパディングに加算されます。そのため、ひとまとまりに見せたいブロックは、リストの間隔に頼る3つのアイテムではなく、独自の小さな間隔を持つ1つのアイテムにまとめます。目に見える隙間が単一の定数に現れない理由、アイテムの境界が間隔の境界でもある理由、グループが条件付きの場合の対処法を扱います。1つのブロックの上に大きな余白ができる、1組の間隔を詰めると他のすべての間隔も変わる、またはヘッダーを複数の遅延アイテムに分割する前に利用してください。commit-a-text-field-on-an-explicit-action— フォーカスが外れたときに値を書き込む編集可能なフィールドでは、ダイアログや画面の回転、意図しないタップでフォーカスが外れると、入力途中の値が保存されます。下書きは保存値をキーとする独立した状態で保持し、ユーザーが確定を選んだ場合にのみ書き込んでください。確定処理で値が正規化されると、確定ボタンを表示するための変更チェックが解除されなくなる理由、下書きを保持すべき場所、2つ目の確定経路が一致させるべき処理を扱います。誰も入力していない切り詰められた値が設定に保存される、保存後も保存ボタンが消えない、または外部で変更された値がフィールドに反映されないときに利用してください。story-reel-auto-pager— ストーリー形式のリールです。カードごとのタイマーで自動的に進むページャー、分割されたプログレスバー、前後に移動するタップ領域、長押しによる一時停止を備えます。各カードのデータはカードごとではなく最初のフレームの前に一度だけ計算し、分割バーの区切り数は、年によってはすべて埋まらないカードリストから取得します。長押し中に進行状況を蓄積しないフレーム差分タイマー、空のonLongPressコールバックがあることで、長押しして離したときに移動せず再開する理由、タップハンドラー内でページャーの現在のページではなく移動先ページを参照する方法、キャプチャしたカードの配色を固定して閲覧者自身のテーマに関係なく同じように表示する方法を扱います。リールのプログレスバーが誤った区切りに移動する、一時停止中の長押しを離すと再開せずすぐに次のカードへ移動する、最初のカードの前ではなくストーリーの途中で読み込みスピナーが表示される、または共有カードが端末のテーマによって違って見えるときに利用してください。hidden-setting-does-not-clear-the-stored-value— OS バージョンや機能の要件を下回ると設定ピッカーからスタイルやエフェクトのオプションが正しく隠されても、そのオプションが示すエフェクトが壊れた状態で表示されることがあります。フラットになる、ぼかされない、または単純に間違った見た目になるのは、ピッカーで制御されるのがユーザーが次に選べる値であり、すでに保存されている設定値ではないためです。バージョンに依存する視覚機能の機能チェックが1か所にあるのに不具合が再現する、または例外を投げずに黙って失敗する修飾子向けのexpect/actualブール値を追加するときに利用してください。
コメント
Markdown 対応