Blog

Jetpack Composeでレスポンシブ対応なら、BoxWithConstraintsが便利

Jetpack Composeを使うことで、今までのxmlベースの実装では対応が難しかったいくつかの内容に対して、実装が容易になることがあります。

その1つがレスポンシブ対応です。

タブレット端末も含めると大小様々な画面サイズのデバイスがあり、また分割画面も考慮すると、対応すべき画面サイズは膨大なものになっています。

最近は折りたたみスマートフォンも増えてきましたね。

xmlベースでのUI構築では、xmlを分けるか、ゴリゴリコードを書いて命令的に更新する必要がありました。

Jetpack Composeは BoxWithConstraints を使うことで簡単に対応することが出来ます。

今回はその使い方について紹介します。

BoxWithConstraints でカラム変更

BoxWithConstraints は基本のコンポーネントの1つである Box とよく似ています。

違いとしては、content の中でminWidth, maxWidth, minHeight, maxHeight といった制約を取得することが出来ます。

例えば、幅が400dp以上あれば横並び、そうでなければ縦並びにするコードを考えます。

@Composable
fun BoxWithConstraintsSample() {
    BoxWithConstraints {
        if (maxWidth >= 400.dp) {
            Row {
                Text(
                    modifier = Modifier
                        .weight(1F)
                        .background(Color.Red)
                        .padding(16.dp),
                    text = "A",
                    textAlign = TextAlign.Center
                )
                Text(
                    modifier = Modifier
                        .weight(1F)
                        .background(Color.Blue)
                        .padding(16.dp),
                    text = "B",
                    textAlign = TextAlign.Center
                )
            }
        } else {
            Column {
                Text(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Red)
                        .padding(16.dp),
                    text = "A",
                    textAlign = TextAlign.Center
                )
                Text(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Blue)
                        .padding(16.dp),
                    text = "B",
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

今回は横幅いっぱいに広げたときに入るかどうかを知りたいので、 maxWidth を利用します。

横幅360dpのPixel 4と横幅480dpのPixel 4 XLで見比べると以下のようになります。

Pixel 4Pixel 4 XL

カルーセルの表示を計算する

カルーセル(横スクロールのリスト)の表示をする際に、横スクロール可能であることがわかりやすいよう、どの画面サイズでも少しだけはみ出して見えるようにしたいことがあります。

ここでは、どの画面サイズでも2.2個分表示されるようにしてみましょう。

@Composable
fun CarouselSample() {
    val items = listOf(
        Color.Red,
        Color.Green,
        Color.Blue
    )

    BoxWithConstraints {
        val cellWidth = maxWidth / 2.2F
        Row(
            modifier = Modifier.horizontalScroll(rememberScrollState())
        ) {
            items.forEachIndexed { index, color ->
                Text(
                    modifier = Modifier
                        .width(cellWidth)
                        .background(color)
                        .padding(16.dp),
                    text = index.toString(),
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

Pixel 4, Pixel 4 XLで見たときは以下のようになります。

Pixel 4Pixel 4 XL

更に横幅が広くなった時を考慮し、セル数を増減させるようにしても良いでしょう。

もっと詳しく

BoxBoxWithConstraints のinterfaceを比較してみると、このようになっています。

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: BoxScope.() -> Unit
): @Composable Unit

@Composable
fun BoxWithConstraints(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: BoxWithConstraintsScope.() -> Unit
): @Composable Unit

modifier, contentAlignment は同じですが、content のscopeが BoxScope から BoxWithConstraintsScope になっています。

@LayoutScopeMarker
@Immutable
interface BoxScope {
    @Stable
    fun Modifier.align(alignment: Alignment): Modifier

    @Stable
    fun Modifier.matchParentSize(): Modifier
}

@Stable
interface BoxWithConstraintsScope : BoxScope {
    val constraints: Constraints
    val minWidth: Dp
    val maxWidth: Dp
    val minHeight: Dp
    val maxHeight: Dp
}

BoxWithConstraintsScopeBoxScope を引き継いでおり、ここにmaxWidth等を持っています。

これにより、BoxWithConstraintscontent 内では maxWidth にアクセスが出来ることになります。

ちなみに、BoxScope 内にModifier の拡張関数を用意することで、Boxのcontent内でのみ使えるModifierを追加しています。(ここでは alignmatchParentSize

Jetpack Composeでは、Kotlinの様々な記法を組み合わせることによって、間違った書き方がしにくくなっています。

また、@Stable は安定しており、勝手に値が書き換わらないことを示しており、@Immutable は一切値が変更されないことを示しています。

これらは、不要なRecomposeを抑えるためのアノテーションです 詳細

BoxBoxWithConstraintsの両方で指定可能なpropagateMinConstraints は、子供にminWidthやminHeightを伝播させるかを決定します。

BoxminWidth, minHeightよりも content が小さい場合、デフォルトの状態だと Alignment に従って小さい状態で配置されますが、propagateMinConstraints=true にすると、minWidth, minHeightに引き伸ばされて表示されます。

fun Sample() {
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color.Red),
        propagateMinConstraints = true
    ) {
        Box(
            modifier = Modifier
                .size(100.dp) // 親のminWidthを引き継ぎ、200dpで表示される
                .background(Color.Blue)
        )
    }
}

まとめ

今までViewのサイズを取得するには一度レンダリングしたあとに値を取得しに行く必要があり、コードが複雑になる傾向がありました。

Jetpack Composeなら、ViewのサイズによってUIを変える必要があっても、宣言的に書くことが出来ます。

これにより、レスポンシブなデザイン実装が非常に楽になったのでは無いでしょうか。

まだ試験運用中ですが、他にもLazyVerticalGrid には GridCells.Adaptive という仕組みがあり、 minSize を指定していい感じにgridレイアウトを組むことが出来ます。

今後もJetpack Composeのトピックスをお届けしていこうと思います。

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも