[jetpack compose]Row/ColumnとConstraintLayoutでどの程度レイアウトの作り方が違うのか


jetpack composeでかんたんなレイアウトをそれぞれ
- Row/Column/Box (※今回Boxは使っていません)
- Constraint

で作成した場合、どのような違いが出るのか見てみました。

ConstraintLayoutのUIの作り方とかはこの記事では触れませんがコード見ていただいてもある程度理解できるかなと思います。

作ってみたのはこちら。
[search, share, home, star]
というラベルが付いたアイコンを並べる

という部分をそれぞれRow/Columを使って実現したパターンとConstraintLayoutを使って実現したパターンとなります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/1a254ef3-7e33-2459-1b09-8d3610d9ec3e.png" width='300px' />

```kotlin
@Composable
fun RowColumnOrConstraintScreen() {
    Column(
        modifier = Modifier.padding(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(text = "row,column composable")
        Card(modifier = Modifier.padding(16.dp)) {
            IconList(
                modifier = Modifier
                    .padding(8.dp)
                    .fillMaxWidth()
            )
        }
        Text(text = "constraint composable")
        Card(modifier = Modifier.padding(16.dp)) {
            IconListConstraint(
                modifier = Modifier
                    .padding(8.dp)
                    .fillMaxWidth()
            )
        }
    }
}

@Composable
@Preview
fun RowColumnOrConstraintScreenPreview() {
    RowColumnOrConstraintScreen()
}
```
こちらがプレビューの内容です

![スクリーンショット 2021-10-22 0.58.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/89d32b4e-532b-609d-bad7-fd6703679e3e.png)

## Row/Column

以下を作ります。

- 基本となるComposable関数
 - `IconLabel`
- search, share, home, starそれぞれを表すComposable関数
 - `IconLabelSearch`, `IconLabelShare`, `IconLabelHome`, `IconLabelStar`
- 一列に並べるComposable関数
 - `IconList`

```kotlin
@Composable
fun IconLabel(imageVector: ImageVector, label: String, modifier: Modifier) {
    Column(
        modifier = modifier.padding(vertical = 8.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Image(
            imageVector = imageVector,
            contentDescription = label,
            modifier = Modifier.padding(horizontal = 16.dp),
        )
        Text(text = label)
    }
}

@Composable
fun IconLabelSearch(modifier: Modifier = Modifier) {
    IconLabel(imageVector = Icons.Rounded.Search, label = "search", modifier = modifier)
}

@Composable
fun IconLabelShare(modifier: Modifier = Modifier) {
    IconLabel(imageVector = Icons.Rounded.Share, label = "share", modifier = modifier)
}

@Composable
fun IconLabelHome(modifier: Modifier = Modifier) {
    IconLabel(imageVector = Icons.Rounded.Home, label = "home", modifier = modifier)
}

@Composable
fun IconLabelStar(modifier: Modifier = Modifier) {
    IconLabel(imageVector = Icons.Rounded.Star, label = "star", modifier = modifier)
}

@Composable
fun IconList(modifier: Modifier = Modifier) {
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.SpaceEvenly,
    ) {
        IconLabelSearch()
        IconLabelShare()
        IconLabelHome()
        IconLabelStar()
    }
}

@Composable
@Preview
fun IconListPreview() {
    IconList(modifier = Modifier.fillMaxWidth())
}
```

こちらがプレビューの内容です。

![スクリーンショット 2021-10-22 0.56.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/3f3e1252-c2d6-d372-74c9-2dd110afec6f.png)

## ConstraintLayout

Row/Columnと同様の構成で以下を作ります。

- 基本となるComposable関数
 - `IconLabelConstraint`
- search, share, home, starそれぞれを表すComposable関数
 - `IconLabelSearchConstraint`, `IconLabelShareConstraint`, `IconLabelHomeConstraint`, `IconLabelStarConstraint`
- 一列に並べるComposable関数
 - `IconListConstraint`

```kotlin
@Composable
fun IconLabelConstraint(imageVector: ImageVector, label: String, modifier: Modifier = Modifier) {
    ConstraintLayout(
        modifier = modifier
    ) {
        val (rateVal) = createRefs()
        Image(imageVector = imageVector, contentDescription = label,
            modifier = Modifier.constrainAs(rateVal) {
                top.linkTo(parent.top, margin = 8.dp)
                start.linkTo(parent.start, margin = 16.dp)
                end.linkTo(parent.end, margin = 16.dp)
            })
        Text(text = label, modifier = Modifier.constrainAs(createRef()) {
            top.linkTo(rateVal.bottom)
            start.linkTo(rateVal.start)
            end.linkTo(rateVal.end)
            bottom.linkTo(parent.bottom, margin = 8.dp)
        })
    }
}

@Composable
fun IconLabelSearchConstraint(modifier: Modifier = Modifier) {
    IconLabelConstraint(imageVector = Icons.Rounded.Search, label = "search", modifier = modifier)
}

@Composable
fun IconLabelShareConstraint(modifier: Modifier = Modifier) {
    IconLabelConstraint(imageVector = Icons.Rounded.Share, label = "share", modifier = modifier)
}

@Composable
fun IconLabelHomeConstraint(modifier: Modifier = Modifier) {
    IconLabelConstraint(imageVector = Icons.Rounded.Home, label = "home", modifier = modifier)
}

@Composable
fun IconLabelStarConstraint(modifier: Modifier = Modifier) {
    IconLabelConstraint(imageVector = Icons.Rounded.Star, label = "star", modifier = modifier)
}

@Composable
fun IconListConstraint(modifier: Modifier = Modifier) {
    ConstraintLayout(
        modifier = modifier
    ) {
        val (search, share, home, star) = createRefs()
        IconLabelSearchConstraint(
            modifier = Modifier.constrainAs(search) {
                top.linkTo(parent.top)
                start.linkTo(parent.start)
                end.linkTo(share.start)
            })
        IconLabelShareConstraint(
            modifier = Modifier.constrainAs(share) {
                top.linkTo(parent.top)
                start.linkTo(search.end)
                end.linkTo(home.start)
            })
        IconLabelHomeConstraint(
            modifier = Modifier.constrainAs(home) {
                top.linkTo(parent.top)
                start.linkTo(share.end)
                end.linkTo(star.start)
            })
        IconLabelStarConstraint(
            modifier = Modifier.constrainAs(star) {
                top.linkTo(parent.top)
                start.linkTo(home.end)
                end.linkTo(parent.end)
            })
    }
}

@Composable
@Preview
fun IconListConstraintPreview() {
    IconListConstraint(modifier = Modifier.fillMaxWidth())
}
```

こちらがプレビューの内容です。
xmlのプレビューと同様に成約のマージンなどが可視化されているのが良いですね。

![スクリーンショット 2021-10-22 1.02.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/44d26f61-a676-dbf6-aeea-79c5b2000ca5.png)

## どちらも同じレイアウトを実現できたが・・・

コードを見ていかがでしたでしょうか？
このレベルのレイアウトならRow/Columnのほうがならべるだけで簡単で良さそうですね。

普段xmlでViewを作っているとついついViewを平たく保つためにConstraintLayoutを使ってしまいがちですが、
実際jetpack composeでConstraintLayoutを使ってみると結構な量で冗長なmodifierを書く必要があり（constrainAsの箇所のことです）、ちょっと読みづらいなと思いました。

## Layout Inspectorでも見てみました

ConstraintLayoutとして木構造になっているので階層構造がこのレベルだとあまり深さの違いはわかりませんでした・・・

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/9f502448-1876-1421-28c7-8c9a6a5918af.png" width="650px"/>

## パフォーマンスについて

公式のドキュメントに言及があります。
ページをそのまま引用させていただきます。

> https://developer.android.com/jetpack/compose/layouts/constraintlayout?hl=ja
注: View システムでは、大規模で複雑なレイアウトを作成する場合のおすすめの方法は ConstraintLayout でした。ネストされたビューよりもフラットなビュー階層のほうがパフォーマンスが優れていたためです。しかし、深いレイアウト階層を効率的に扱える Compose では、このような懸念はありません。
注: Compose の特定の UI に ConstraintLayout を使用するかどうかは、デベロッパーの好みによります。Android View システムでは、より高パフォーマンスのレイアウトを作成する方法として ConstraintLayout が使用されていましたが、この点は Compose では問題になりません。選択する必要がある場合は、ConstraintLayout がコンポーザブルの読みやすさと保守性に役立つかどうかを検討します。


## ConstraintLayoutはマルチプラットフォームではない

ConstraintLayoutは現在マルチプラットフォーム化はされておらずAndroidで使えるライブラリとなっています。

Row/Column/Boxといった標準UIで構成できるようにしておくことで今後、
jetpack compose for Desktopやfor Webなどにも適用できるような基礎力が身につくのかなと思いました。

## まとめ

実際ConstraintLayoutが煩雑になりやすそうなイメージを持ったので
- 基本はRow/Column/BoxでUIを作っていく
- 局所的に成約を設定したい場合にConstraintLayout使っても良さそう

くらいの認識になりました。