mcpField
O
ordering-modifier-chains

ordering-modifier-chains

github.com/skydoves開発520129 回インストール19 週間前に更新
リポジトリ

ordering-modifier-chains について

Jetpack Compose開発者向けのスキルです。Modifierチェーンの順序が描画、レイアウト、タップ領域、クリッピングに与える影響を説明し、症状に応じた並べ替えを示します。

主な機能

  1. 01Modifierチェーンを入れ子として読む方法
  2. 02意図した塗りに合わせたpaddingとbackgroundの順序
  3. 03clickableのタップ領域の制御
  4. 04背景描画に対するclipの配置
  5. 05graphicsLayerの適用範囲の説明

活用シーン

  1. 01背景が意図した範囲に描画されない問題の調査
  2. 02ボタンのタップ領域やリップル範囲の修正
  3. 03背景を形状に合わせてクリップ
  4. 04意図したサブツリーへのアルファや変換の適用

インストール

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

$npx skills add https://github.com/skydoves/compose-performance-skills --skill ordering-modifier-chains

インストール後、Claude Code で/ordering-modifier-chainsを入力して読み込まれたか確認します。

詳細

Modifier チェーンの順序 — padding(8.dp).background(Red) と background(Red).padding(8.dp) が異なる理由

各 Modifier は関数として次の Modifier をラップするため、順序が重要です。Modifier.padding(8.dp).background(Red) では、最初に制約が縮小されるため、背景色はパディング領域の内側に描画され、周囲の 8 dp の余白は赤くなりません。反対に、Modifier.background(Red).padding(8.dp) では、コンテンツを内側に配置する前に、親の全境界に赤色が描画されます。順序の誤りは、キーの指定漏れに次いで Compose UI で最もよくあるバグです。このスキルでは、Claude がチェーンを上から順に読み、正しい順序に並べ替える方法を説明します。

このスキルを使う場面

  • 開発者から「クリック領域が表示されているボタンの範囲を超えるのはなぜ?」「リップルが間違った形で広がるのはなぜ?」「背景が間違った領域に描画されるのはなぜ?」「Modifier の順序は重要?」と聞かれたとき。
  • コードレビューで、Modifier.padding(...).clickable { }、Modifier.background(...).clip(...)、Modifier.alpha(state.value).graphicsLayer { } のようなチェーンが見つかったとき。
  • ボタンのタップ領域が周囲のパディングにはみ出す、または背景に角丸が適用されないとき。
  • 親の clip が子の描画をマスクできない、または graphicsLayer { alpha = 0.5f } が兄弟要素にも半端に適用されるとき。
  • 開発者がパフォーマンス向上のために、Modifier チェーンを remember { Modifier.… } でホイスティングすることを検討しているとき。

このスキルを使わない場面

  • チェーンの順序が正しく、症状の原因が別のところにあるとき(状態が誤ったフェーズで読み取られている、遅延レイアウトでキーが不足しているなど)。まず ../../recomposition/deferring-state-reads/SKILL.md または ../../lists/optimizing-lazy-layouts/SKILL.md を使って診断してください。
  • カスタム Modifier 自体に問題があるとき(レガシーな composed { }、差分処理の不足など)→ ../migrating-to-modifier-node/SKILL.md を参照してください。
  • カスタム Modifier を作るべきかどうかが質問のとき。このスキルでは、すでに組み込み Modifier のチェーンがあり、正しい順序に並べる必要があることを前提としています。

前提条件

  • Modifier の構成に関する用語(padding、background、clip、clickable、graphicsLayer)の基本的な知識。
  • サポートされている Compose UI の任意のバージョン。Modifier の順序に関するセマンティクスは 1.0 以降、安定しています。
  • ホイスティングのパフォーマンスに関して最終的な主張をする場合は、実機でのリリースビルドを使用してください(skydoves のホットテイク #5:デバッグビルドは当てになりません)。

ワークフロー

  • 1. 症状を正確に特定します。 次のいずれかに当てはめます:描画領域が間違っている、クリック/ポインター領域が間違っている、リップルの境界が間違っている、クリップ領域が間違っている、パディングの測定が間違っている、graphicsLayer のスコープが間違っている。それぞれに対応する並べ替えがあります。

  • 2. チェーンを上から下へ読みます。各モディファイアの効果は、その下にあるすべてに適用されます。 チェーンをネストした関数呼び出しとして捉えます。Modifier.a().b().c() は「a が(b が(c を包む))を包む」という意味です。最上部のモディファイアは最も外側のラッパーで、最下部のモディファイアはコンテンツに最も近いものです。

  • 3. 症状に対応する標準的な並べ替えを適用します。 以下のパターンでは、最も一般的な 4 つのケース(padding と background、clickable の配置、background の前に置く clip、graphicsLayer のスコープ)を扱います。

  • 4. 次のモディファイアには特に注意します。順序は見た目の問題ではなく、実際の動作に影響します。

    • clickable — ポインターのヒット領域は、チェーン内のその位置における境界です。その上(外側)にあるものは含まれ、その下(内側)にあるものは含まれません。
    • clip — その下(内側)にあるすべてをクリップします。その上(外側)にあるものはクリップされません。
    • background — チェーン内のその位置における境界全体に描画します。
    • padding — その下にあるすべてに渡される利用可能なスペースを減らします。
    • graphicsLayer — その下にあるすべてを描画レイヤーで包みます(アルファ、変換、クリッピングはすべて、その下のサブツリーに適用されます)。
  • 5. タッチターゲットのアクセシビリティを考慮します。 Material の 48dp の最小タッチターゲットは、clickable を視覚的なパディングより上に置くことで実現できます。これにより、パディング部分もタップ可能になります。その代わり、パディング部分でもリップルが発生します。視覚的な領域だけをクリック可能にする場合は、clickable を padding より下に置きます。

  • 6. パフォーマンス向上を目的に、remember を使って Modifier チェーンを外側に引き出すのは避けます。 Compose は構造的に等しい Modifier チェーンを内部で既にインターン化します。remember { Modifier.fillMaxWidth().padding(16.dp) } は、プロファイルでほとんど確認できないマイクロ最適化です。チェーンを外側に引き出さないことで測定可能な回帰が起きることを、FrameTimingMetric ベンチマークで事前に証明できない限り、このような remember を追加してはいけません。アイテムのラムダ内のアロケーションに関するより広範な説明は、../../lists/optimizing-lazy-layouts/SKILL.md を参照してください。

パターン

パターン:background を padding の外側に置く場合と内側に置く場合
// WRONG (when the intent is "padded box with red fill")
Box(Modifier.padding(8.dp).background(Color.Red))
// WRONG because: padding is applied first; background then paints across the inner (padded) bounds — the result is a smaller red region, with no red in the padded margin. If the design intent was "red card with 8dp internal padding", red is on the wrong side of the padding.
// RIGHT — red fills the box; content is inset by 8dp.
Box(Modifier.background(Color.Red).padding(8.dp)) { /* content */ }

逆の組み合わせも有効ですが、開発者が周囲のパディングに赤色を広げたくない場合(小さな内側バッジなど)に限ります。「内部にパディングがあるカード」では、background → padding をデフォルトにします。

パターン:clickable を padding の上に置く場合と下に置く場合(タッチターゲットと視覚的な領域)
// WRONG (when the click should fire only on the visible icon)
Box(Modifier.padding(16.dp).clickable { onTap() })
// WRONG because: clickable's hit region is its position in the chain — and at this position, the bounds INCLUDE the padding. Taps in the padding fire onTap and ripple bounces past the visible icon.
// RIGHT — click only fires on the visible content area.
Box(Modifier.clickable { onTap() }.padding(16.dp))
// RIGHT — accessibility 48dp touch target: extend the tap area into the padding intentionally.
Box(
    Modifier
        .padding(8.dp)            // outer spacing
        .clickable { onTap() }    // tap fires across the next size step
        .padding(16.dp)           // visual padding inside the tap area
)

Material の 48dp の最小タッチターゲットは、外側のスペースの内側で、視覚的なパディングの外側に clickable が配置されるようにすることで実現できます。デザインの意図に合う方を選んでください。どちらの順序が「より正しい」ということはなく、異なる製品の動作を表します。

パターン:background の前に clip を置く場合と後に置く場合
// WRONG (rounded card with red fill)
Box(Modifier.background(Color.Red).clip(RoundedCornerShape(8.dp)))
// WRONG because: background paints first across square bounds; clip then applies to children only. The red rectangle is unclipped — corners stay square.
// RIGHT — clip wraps the background, so the red is rounded.
Box(Modifier.clip(RoundedCornerShape(8.dp)).background(Color.Red))

クリップと背景の意味を組み合わせるモディファイアでは、Modifier.background(color = ..., shape = ...) を使うと 1 つのノードで両方を実行でき、順序を考慮する必要がなくなります。

// RIGHT — single modifier handles shape clipping and fill atomically.
Box(Modifier.background(Color.Red, RoundedCornerShape(8.dp)))
パターン:graphicsLayer はその下にあるすべてを包む
// RIGHT — alpha applies to the entire subtree (background + content).
Box(
    Modifier
        .graphicsLayer { alpha = 0.5f }
        .background(Color.Red)
        .padding(8.dp)
) { Text("Half-faded card") }
// WRONG — alpha only fades the content; background is fully opaque.
Box(
    Modifier
        .background(Color.Red)
        .padding(8.dp)
        .graphicsLayer { alpha = 0.5f }
) { Text("Surprising opacity") }
// WRONG because: graphicsLayer wraps only what's below it; the background painted above is not in its layer.

これは、間違ったフェーズでのアニメーション読み取りに次いでよくある「フェードが変に見えるのはなぜ?」というバグです。「この視覚的なユニット全体をフェードさせる」ことが目的なら、graphicsLayer をチェーンの最上部(またはその近く)に配置します。

パターン:graphicsLayer 内の clickable(ヒットテストでの思わぬ挙動)
// RIGHT — graphicsLayer { } wraps clickable; ripple is captured into the layer.
Box(
    Modifier
        .graphicsLayer { alpha = 0.9f }
        .clickable { onTap() }
        .padding(16.dp)
)

graphicsLayer はヒットテストのジオメトリーを変更しません。graphicsLayer が clickable を包んでいるかどうかにかかわらず、clickable はチェーン内の自身の位置を基準にヒットテストを行います。graphicsLayer が変換(平行移動、スケール、回転)を適用する場合、ヒットテストはレイアウト後(変換前)の境界に従います。変換後のヒットテストには、明示的なヒットテストを行う Modifier.pointerInput { } が必要です。より広範なフェーズの説明については、../../recomposition/deferring-state-reads/SKILL.md を参照してください。

パターン:Modifier チェーンのホイスト — 通常は効果がありません
// OK but rarely necessary
@Composable
fun SnackList(snacks: ImmutableList<Snack>) {
    val rowModifier = remember { Modifier.fillMaxWidth().padding(16.dp) }
    LazyColumn {
        items(snacks, key = { it.id }) { snack -> SnackRow(snack, rowModifier) }
    }
}
// Equally fine in practice
@Composable
fun SnackList(snacks: ImmutableList<Snack>) {
    LazyColumn {
        items(snacks, key = { it.id }) { snack ->
            SnackRow(snack, Modifier.fillMaxWidth().padding(16.dp))
        }
    }
}

Compose は構造的に等しい Modifier チェーンを内部で共有します。上記の 2 つのスニペットは、各アイテムの位置で equals と互換性のある同一のチェーンを生成します。remember によるホイストで節約できるのは、再コンポジションあたりアイテムごとに最大 1 回のアロケーションです。合成マイクロベンチマークでは測定できますが、実際の画面では FrameTimingMetric に現れません。FrameTimingMetric の回帰を示せない限り、「パフォーマンスのため」に remember の行を追加してはいけません。より大きな効果が得られる Modifier.composed { } ファクトリーの修正については、../migrating-to-modifier-node/SKILL.md を参照してください。

パターン:padding の後に size — 測定可能な差が出ない順序
// Both compile; both are valid; they mean different things.
Box(Modifier.size(100.dp).padding(8.dp))   // 100dp box, 8dp inner padding → 84dp content area
Box(Modifier.padding(8.dp).size(100.dp))   // 100dp content area, 8dp outer margin → 116dp total

size はチェーン内のその位置で境界を制約し、padding は下にあるすべての要素に渡す利用可能な領域から差し引きます。上から下へ読みます。この順序はレイアウトを表すものであり、「順番を変えても同じ」ではありません。

必須ルール

  • 必須: Modifier チェーンを上から下へ読み、各 Modifier がその下にあるすべての要素をラップするものとして考えてください。
  • 必須: クリック可能な領域を目に見える範囲だけにする場合は、clickable を padding の後(つまり、チェーン内で clickable が padding の下)に配置してください。パディング部分もタップ可能にする必要があるデザイン(Material の 48dp タッチターゲット)の場合は、clickable を padding の前に配置してください。
  • 必須: 背景の描画をクリップするには、clip を background の前に配置してください。または Modifier.background(color, shape) を使って、順序の問題を完全に避けてください。
  • 必須: 変換やアルファを背景を含む視覚要素全体に適用する必要がある場合は、チェーンの上の方に graphicsLayer を配置してください。
  • 禁止: FrameTimingMetric の回帰を示せない限り、「パフォーマンスのため」だけに remember { Modifier.… } で Modifier チェーンをホイストしてはいけません。Compose は同一のチェーンをすでに内部で共有しており、節約できるアロケーションがプロファイルに現れることはほとんどありません。
  • 禁止: 順序の問題を「修正」するために Modifier.composed { } ファクトリーを追加してはいけません。カスタム Modifier を特定の位置に置く必要がある場合は、直接 Modifier.Node にできます。../migrating-to-modifier-node/SKILL.md を参照してください。
  • 推奨: アクセシビリティ(最小 48dp)のためにタップ可能にするアイテムでは、padding → clickable → padding の明示的なサンドイッチを使用してください。外側のスペース、視覚上のパディングを含むサイズのクリック領域、コンテンツ用の内側のパディングを作れます。
  • 推奨: 両方が必要な場合は、clip(shape).background(color) の組み合わせより Modifier.background(color = ..., shape = ...) を使用してください。

検証

  • 描画領域(背景、ボーダー)がデザインの意図と一致しています。角丸から赤色が予期せずはみ出したり、意図しないパディングの余白に赤色が入ったりしていません。
  • クリック/リップル領域がデザインの意図と一致しています。パディング部分のタップでイベントが発生するかどうかが、意図せずではなく明確に決まっています。
  • clip がその下にあるすべてのレイヤーをマスクしています。すべてのテーマでクリップされた角が正しく表示されます。
  • graphicsLayer { alpha = ... }(または scaleX、rotationZ など)が意図したサブツリーだけに作用しています。想定どおり、背景とコンテンツが一緒にフェードします。
  • Modifier チェーンをホイストした場合は、release + R8 + 実機での FrameTimingMetric マクロベンチマークにより、ホイストしないバージョンと比べて測定可能な改善が示されています。そうでなければ、remember を削除してください。
  • チェーンの順序に関する問題への対処として Modifier.composed { } が追加されていません(モジュール内を再度 grep してください)。

参考資料

コメント