Compose 修飾子

修飾子を使用すると、コンポーザブルを装飾または拡張できます。修飾子では、次のようなことができます。

  • コンポーザブルのサイズ、レイアウト、動作、外観を変更する
  • ユーザー補助ラベルなどの情報を追加する
  • ユーザー入力を処理する
  • 要素をクリック可能、スクロール可能、ドラッグ可能、ズーム可能にするなど、高レベルの操作を追加する

修飾子は標準の Kotlin オブジェクトです。Modifier クラス関数のいずれかを呼び出して修飾子を作成します。

@Composable
private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

色付きの背景で、テキストの周囲にパディングを適用した 2 行のテキスト。

こうした関数を連鎖させてコンポーズできます。

@Composable
private fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxWidth()
    ) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

テキストの色付きの背景がデバイスの幅全体に拡張されている。

上記のコードでは、さまざまな修飾子関数を一緒に使用しています。

  • padding は、要素の周囲にスペースを挿入します。
  • fillMaxWidth は、コンポーザブルを親から与えられた最大幅に合わせて調整します。

おすすめの方法は、すべてのコンポーザブルが modifier パラメータを受け取り、その修飾子を、UI を出力する最初の子に渡すことです。これにより、コードの再利用性が向上し、動作が予測可能で直感的になります。詳しくは、Compose API のガイドライン「要素が修飾子パラメータを受け取って準拠する」についての説明をご覧ください。

修飾子の順序の重要性

修飾子関数の順序は重要です。各関数は前の関数が返す Modifier を変更するため、順序は最終結果に影響を与えます。次の例をご覧ください。

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

エッジの周囲のパディングも含め、エリア全体がクリックに反応

上記のコードでは、padding 修飾子が clickable 修飾子の後に適用されるため、周囲のパディングを含むエリア全体がクリック可能となります。修飾子の順序が逆の場合、padding で追加されたスペースはユーザー入力に反応しません。

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

レイアウトのエッジ周囲のパディングがクリックに反応しない

組み込み修飾子

Jetpack Compose には、コンポーザブルの装飾や拡張に役立つ組み込み修飾子のリストが用意されています。レイアウトの調整に使用する一般的な修飾子は次のとおりです。

paddingsize

Compose で提供されるレイアウトは、デフォルトでは子をラップしていますが、size 修飾子を使用してサイズを設定できます。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

指定したサイズがレイアウトの親の制約を満たさない場合、そのサイズが適用されないことがあります。親の制約に関係なくコンポーザブルのサイズを固定する必要がある場合は、requiredSize 修飾子を使用します。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

子の画像が親の制約よりも大きい

この例では、親 height100.dp に設定されていても、requiredSize 修飾子が優先されるため、Image の高さは 150.dp になっています。

親によって許可されているすべての高さを子レイアウトで埋めるには、fillMaxHeight 修飾子を追加します(Compose では、fillMaxSizefillMaxWidth も提供されています)。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

画像の高さが親と同じ

要素全体にパディングを追加するには、padding 修飾子を設定します。

テキストのベースラインの上にパディングを追加して、レイアウトの上部からベースラインまで一定の距離を空ける場合は、paddingFromBaseline 修飾子を使用します。

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

上にパディングが設定されたテキスト

オフセット

レイアウトを元の位置に対して相対的に配置するには、次のように offset 修飾子を追加し、x 軸と y 軸でオフセットを設定します。オフセットは、正の数でも負の数でもかまいません。paddingoffset の違いは、offset をコンポーザブルに追加しても測定値自体は変更されない点です。

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

親コンテナの右側に移動したテキスト

offset 修飾子は、レイアウト方向に従って横方向に適用されます。正の offset を設定する場合、左から右方向のコンテキストでは要素が右に移動し、右から左方向のコンテキストでは要素が左に移動します。レイアウト方向を考慮せずにオフセットを設定する必要がある場合は、absoluteOffset 修飾子をご覧ください。この修飾子を使用すると、正のオフセット値を設定した場合に要素が常に右へ移動します。

offset 修飾子には、オフセットをパラメータとして受け取る offset と、ラムダで受け取る offset という 2 つのオーバーロードがあります。それぞれのオーバーロードを使用すべき状況と、パフォーマンスを最適化する方法について詳しくは、Compose のパフォーマンス - 可能な限り読み取りを延期するをご覧ください。

Compose でのスコープの安全性

Compose には、特定のコンポーザブルの子に適用される場合にのみ使用できる修飾子があります。Compose はカスタム スコープによって、これを可能にしています。

たとえば、Box サイズに影響を与えずに子を親の Box と同じサイズにするには、matchParentSize 修飾子を使用します。matchParentSizeBoxScope でのみ使用できます。そのため、この修飾子を使用できるのは、親の Box 内の子に限られます。

スコープの安全性により、他のコンポーザブルやスコープでは機能しない修飾子を追加できなくなるため、試行錯誤する時間を節約できます。

スコープ修飾子は、子に関して親が知っておくべき情報を親に通知します。これは、一般に親データ修飾子とも呼ばれます。その内部構造は汎用修飾子とは異なりますが、使用方法の観点から見るとこれらの違いは重要ではありません。

matchParentSizeBox

前述のように、Box サイズに影響を与えずに子レイアウトを親 Box と同じサイズにする場合は、matchParentSize 修飾子を使用します。

matchParentSizeBox スコープ内でしか使用できず、Box コンポーザブルの直接の子にのみ適用されます。

以下の例では、子 Spacer は親 Box からサイズを取得し、親はそのサイズを最も大きな子(この場合は ArtistCard)から取得しています。

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(
            Modifier
                .matchParentSize()
                .background(Color.LightGray)
        )
        ArtistCard()
    }
}

コンテナいっぱいに表示されたグレーの背景

matchParentSize ではなく fillMaxSize を使用した場合、Spacer は親が使用できるすべてのスペースを使用します。この場合、親は使用可能なスペース全体に拡大します。

画面全体に表示されたグレーの背景

weightRowColumn

前のセクションのパディングとサイズで説明したように、デフォルトでは、コンポーザブルのサイズは自身がラップするコンテンツによって定義されます。RowScopeColumnScope でのみ使用可能な weight 修飾子を使用すると、コンポーザブルのサイズを親の内部で柔軟に変動するように設定することもできます。

たとえば、2 つの Box コンポーザブルが含まれる Row があるとします。最初のボックスには、2 番目のボックスの 2 倍の weight が指定されているため、幅も 2 倍になります。Row の幅は 210.dp なので、最初の Box の幅は 140.dp、2 番目のボックスの幅は 70.dp になります。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

画像の幅はテキストの 2 倍

修飾子の抽出と再利用

複数の修飾子を連鎖させて、コンポーザブルを装飾または拡張できます。このチェーンは、単一の Modifier.Elements の不変の順序付きリストを表す Modifier インターフェースを介して作成されます。

Modifier.Element は、レイアウト、描画、グラフィックの動作、ジェスチャー関連のあらゆる動作、フォーカスとセマンティクスの動作、デバイス入力イベントなどの個別の動作を表します。これらの順序は重要です。最初に追加された修飾子要素が最初に適用されます。

同じ修飾子チェーンのインスタンスを複数のコンポーザブルで再利用し、変数に抽出してより高いスコープにホイスティングすると便利な場合があります。コードの読みやすさやアプリのパフォーマンスが改善されることがあります。これには次のような理由があります。

  • 修飾子を使用するコンポーザブルに対して再コンポーズが行われると、修飾子の再割り当てが繰り返されない
  • 修飾子チェーンは非常に長く複雑になる可能性があるため、同じインスタンスのチェーンを再利用すると、Compose ランタイムで比較時に必要となるワークロードを軽減できる
  • この抽出により、コードベース全体で雑多なコードが減り、コードの整合性、保守性が向上します。

修飾子を再利用する際のベスト プラクティス

独自の Modifier チェーンを作成して抽出し、これらを複数のコンポーザブル コンポーネントで再利用します。修飾子はデータに似たオブジェクトであるため、修飾子を保存することはまったく問題ありません。

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

頻繁に変化する状態を監視しているときの修飾子の抽出と再利用

アニメーションの状態や scrollState など、コンポーザブル内で頻繁に変化する状態を監視する場合、再コンポーズが頻繁に行われる可能性があります。この場合、修飾子は再コンポーズのたびに割り当てられ、場合によってはフレームごとに割り当てられます。

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // Creation and allocation of this modifier will happen on every frame of the animation!
        modifier = Modifier
            .padding(12.dp)
            .background(Color.Gray),
        animatedState = animatedState
    )
}

代わりに、次のように同じインスタンスの修飾子を作成、抽出、再利用し、コンポーザブルに渡します。

// Now, the allocation of the modifier happens here:
val reusableModifier = Modifier
    .padding(12.dp)
    .background(Color.Gray)

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // No allocation, as we're just reusing the same instance
        modifier = reusableModifier,
        animatedState = animatedState
    )
}

スコープ設定されていない修飾子の抽出と再利用

修飾子は、スコープ設定しないことも、特定のコンポーザブルに対してスコープ設定することもできます。スコープ設定されていない修飾子の場合は、任意のコンポーザブルの外部から単純な変数として修飾子を簡単に抽出できます。

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

@Composable
fun AuthorField() {
    HeaderText(
        // ...
        modifier = reusableModifier
    )
    SubtitleText(
        // ...
        modifier = reusableModifier
    )
}

これは、Lazy レイアウトと組み合わせると特に便利です。通常の場合には、潜在的に重要な項目には同一の修飾子を使用することをおすすめします。

val reusableItemModifier = Modifier
    .padding(bottom = 12.dp)
    .size(216.dp)
    .clip(CircleShape)

@Composable
private fun AuthorList(authors: List<Author>) {
    LazyColumn {
        items(authors) {
            AsyncImage(
                // ...
                modifier = reusableItemModifier,
            )
        }
    }
}

スコープ設定された修飾子の抽出と再利用

特定のコンポーザブルにスコープ設定する修飾子を処理する場合は、可能な限り高いレベルに修飾子を抽出し、必要に応じて再利用します。

Column(/*...*/) {
    val reusableItemModifier = Modifier
        .padding(bottom = 12.dp)
        // Align Modifier.Element requires a ColumnScope
        .align(Alignment.CenterHorizontally)
        .weight(1f)
    Text1(
        modifier = reusableItemModifier,
        // ...
    )
    Text2(
        modifier = reusableItemModifier
        // ...
    )
    // ...
}

抽出されてスコープ設定された修飾子は、同じスコープの直接の子にのみ渡す必要があります。この制限が重要な理由について詳しくは、Compose でのスコープの安全性のセクションをご覧ください。

Column(modifier = Modifier.fillMaxWidth()) {
    // Weight modifier is scoped to the Column composable
    val reusableItemModifier = Modifier.weight(1f)

    // Weight will be properly assigned here since this Text is a direct child of Column
    Text1(
        modifier = reusableItemModifier
        // ...
    )

    Box {
        Text2(
            // Weight won't do anything here since the Text composable is not a direct child of Column
            modifier = reusableItemModifier
            // ...
        )
    }
}

抽出された修飾子のさらなる連鎖

.then() 関数を呼び出すことで、抽出された修飾子チェーンをさらに連鎖させるか、追加することができます。

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

// Append to your reusableModifier
reusableModifier.clickable { /*...*/ }

// Append your reusableModifier
otherModifier.then(reusableModifier)

修飾子の順序が重要であることに留意してください。

詳細

修飾子の全リストと、各修飾子のパラメータとスコープを紹介します。

修飾子の使用方法について詳しくは、Compose の基本レイアウトの Codelab を参照するか、最新の Android リポジトリをご覧ください。

カスタム修飾子とその作成方法について詳しくは、カスタム レイアウト - レイアウト修飾子の使用のドキュメントをご覧ください。