🤔

Jetpack Composeでリストを入れ子にするときは子要素に工夫が必要

2023/06/12に公開2

こんにちは、horitamonです。

昨今のAndroid開発でスタンダードになりつつあるJetpack Compose。
僕が開発に参画しているVoicyアプリのAndroid版でも、最近積極的に採用しています。

今回はComposeを使ってリスト上のViewを実装する際に困ったことと、その対処方法についてまとめてみます。

LazyColumnの中にLazyColumnが入れられない

同じレイアウトのViewが連なるリストの中に、さらに同様に同じレイアウトのViewのリストが入れ子になったレイアウトを実装しようとしたときのこと。


オレンジ枠が親となるSectionのリスト、緑枠が子となるSoundのリスト

まだまだCompose初心者の私は、「LazyColumnの中にLazyColumnを入れればいいのでは?」と、こんな感じで安直に実装してしまいました。

@Composable
fun SectionList(
    sections: List<Section>
) {
    LazyColumn(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        items(sections) { section ->
            Column(Modifier.padding(horizontal = 24.dp)) {
                Text(
                    text = section.name ?: "",
                    fontSize = 18.sp,
                    fontWeight = FontWeight.Bold
                )
                SoundList(
                    sounds = section.soundList
                )
            }
        }
    }
}

@Composable
fun SoundList(
    sounds: List<Sound>
) {
    LazyColumn {
        items(sounds) { sound ->
            SoundItem(
                title = sound.title,
                duration = sound.duration,
                playCount = sound.playCount)
            Divider(thickness = 1.dp, color = Color.Gray)
        }
    }
}

しかしここで現れるエラー。

Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).

なるほど、「垂直方向にスクロール可能なViewの高さの制約がない」ことが怒られているみたいです。
確かに設定してないです。
うまいこと設定してあげましょう。

じゃあどうする?

同様のエラーの対処法を調べてみると、LazyColumnの子要素のリストを分解して一つ一つの要素を定義してあげようという例が多いようです。
しかし今回実装したいレイアウトは完全に同じレイアウトの繰り返しなので、その方法は避けたい…
というわけで、子要素のリストを分解せずに表示する方法を探ってみます。

1.高さの数値を指定する

まずは指摘されたことに素直に従って、子要素のLazyColumnの引数にModifierを指定して高さを指定しましょう。

@Composable
fun SoundList(
    sounds: List<Sound>
) {
    LazyColumn(modifier = Modifier.height(250.dp)) {
        items(sounds) { sound ->
            SoundItem(
                title = sound.title,
                duration = sound.duration,
                playCount = sound.playCount)
            Divider(thickness = 1.dp, color = Color.Gray)
        }
    }
}

例外は出なくなりましたが、これだと指定した高さよりリストすべての高さ合計が大きい場合、LazyColumnで指定した範囲内スクロールされてしまいます。

スクロールは親のViewで担ってくれるので、そもそもここまでスクロールできる必要はありませんね。
他の方法を探ってみましょう。

2.リスト表示にColumnを使用しない

「スクロール可能なView」だからエラーが出るのであれば、スクロールできないViewを子要素にしましょう。
今回はシンプルにrepeatで繰り返し描画するようにしてみます。

@Composable
fun SoundList(
    sounds: List<Sound>
) {
    repeat(sounds.size) { iteration ->
        sounds.getOrNull(iteration)?.let { sound ->
            SoundItem(
                title = story.title,
                duration = story.duration,
                playCount = story.playCount)
            Divider(thickness = 1.dp, color = Color.Gray)
        }
    }
}


はい、今度はしっかり全体がスクロールできるように描画できました。

まとめ

基本的にLazyColumn/LazyRowを使用する際は、子要素のサイズを有限にするか、スクロール不可にすることがルールであることを知りました。
そもそもLazyColumnやColumnを使わずとも、Compose関数を繰り返し呼んであげればリストを表示できるということに気づくのが遅かったです…

Voicyテックブログ

Discussion

あっぷる中谷あっぷる中谷

もっと単純な話でスクロールできる要素の中にスクロールできる要素を入れているからエラーがでているという感じですね(一応やりようはある)

LazyColumnの子要素がスクロールする必要がないのであればitems内でforEachすればサクッと書けると思います。

ほーりーほーりー

ありがとうございます!
確かにスクロールできる要素を入れ子にするとエラー出ちゃいますね。

入れ子になるときはCompose関数分けずに親のitems内でforEachするのもありですね!