さて今回はリストについて説明していきます。リストは、同じ見た目の項目を複数並べて表示するためのUIです。設定画面や連絡先の表示、ファイルやコンテンツの一覧表示など、あらゆる場面で使います。従来はRecyclerView
で実現していましたが、正直、かなり面倒でしたし、初心者にはとっつきにくくもありました。しかしJetpack Composeならとても簡単に実装できるので、ぜひ試してみてください。
Lazyコンポーザブル
Jetpack Composeのリストは、 androidx.compose.foundation.lazy
パッケージに定義されているLazyコンポーザブルを使って実現します。アイテムを縦に並べる場合は LazyColumn()
、横に並べる場合はLazyRow()
、項目を格子状に並べる場合はLazyVerticalGrid()
を使います。なおLazyVerticalGrid()
は2022年1月時点ではExperimentalFoundationApiなので、今後仕様変更などがあるかもしれません。
「コンポーネントを配置する」で説明したColumn
やRow
との違いは、画面描画時にすべてのアイテムを描画するかどうかです。Column
やLow
は画面初期化時にすべてのアイテムを描画するのに対して、Lazyコンポーザブルは画面に見える範囲のアイテムだけを描画します。隠れているアイテムは、スクロールによって画面内に見えるようになるタイミングで初めて描画されます。そのため、アイテム数が多い場合にもシステムに負荷がかかりません。表示するアイテム数が多い場合や、アイテム数が事前に決まっていない場合は、Lazyコンポーザブルを使うようにします。
簡単なリスト
初めに、あらかじめ決まっている文字列を表示するだけの簡単なリストを作ってみましょう。
@Composable
fun List1() {
val fruits = listOf("Apple", "Orange", "Grape", "Peach", "Strawberry")
LazyColumn {
items(fruits) { fruit ->
Text(text = "This is $fruit")
}
}
}
どうでしょう? すっごく簡単ですよね。私は初めて見たとき感動しました。今までRecyclerView
でAdapter作ったりViewHolder作ったり、アイテム表示用のXMLリソースを作ったりしていたのが噓みたいです。なんとなく見た目で理解できると思いますが、一通り解説します。
LazyColumn()
の定義は以下のようになっています。
@Composable
fun LazyColumn(
modifier: Modifier? = Modifier,
state: LazyListState? = rememberLazyListState(),
contentPadding: PaddingValues? = PaddingValues(0.dp),
reverseLayout: Boolean? = false,
verticalArrangement: Arrangement.Vertical? = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal? = Alignment.Start,
flingBehavior: FlingBehavior? = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean? = true,
content: (@ExtensionFunctionType LazyListScope.() -> Unit)?
): Unit
必須の引数はcontent
一つだけで、通常は先ほどの例のように、"LazyColumn"
の後にラムダ式で記述します。content
のラムダ式では、リストに表示するアイテムを定義します。アイテムがList
やArray
で保持されている場合は、items()
を使います。items()
の定義は以下の通りです。
inline fun <T : Any?> LazyListScope?.items(
items: List<T?>?,
noinline key: ((item) -> Any)? = null,
crossinline itemContent: (@Composable @ExtensionFunctionType LazyItemScope.(item) -> Unit)?
): Unit
引数item
にList
を渡し、itemContent
に各アイテムを表示するコンポーザブルを書きます。もう一度最初のサンプルの該当部分を見てみましょう。
LazyColumn {
items(fruits) { fruit ->
Text(text = "This is $fruit")
}
}
外側のラムダ式がLazyColumn()
のcontent
引数で、items()
を使ってfruits
リストを渡しています。内側のラムダ式はitems()
のitemContent
引数で、Text
を使ってfruits
の各要素を表示しています。
見た目を改善する
先ほどの例で、リスト表示を非常に簡単に実現できることは確認できましたが、実際のアプリに使うにはさすがに簡素すぎます。そこで、見た目を少し改善してみたいと思います。
今回は、果物の名前に加えてアイコンも表示してみます。次のようなデータを用意しました。
data class Fruits(val name: String, val imageResource: Int)
val fruits = listOf(
Fruits("Apple", R.drawable.apple),
Fruits("Orange", R.drawable.orange),
Fruits("Grape", R.drawable.grape),
Fruits("Peach", R.drawable.peach),
Fruits("Strawberry", R.drawable.strawberry),
)
文字列と画像リソースをセットで保持しています。これをコンポーザブル関数に引数で渡してリストを描画するようにします。
@Composable
fun List2(fruits: List<Fruits>) {
LazyColumn(
modifier = Modifier.background(Color.LightGray),
verticalArrangement = Arrangement.spacedBy(5.dp),
contentPadding = PaddingValues(5.dp)
) {
items(fruits) { fruit ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(5.dp))
) {
Image(
painter = painterResource(id = fruit.imageResource),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 10.dp)
.size(50.dp)
)
Text(
text = fruit.name,
fontSize = 18.sp,
)
}
}
}
}
LazyColumn
のverticalArrangement
引数では、Arrangement.spacedBy()を使ってリストアイテム同士の間隔を指定しています。contentPadding
引数では、PaddingValues()
を使ってアイテムの周囲の余白を指定しています。左右の余白とAppleの上の余白はcontentPadding
で、AppleとOrangeの間などのアイテム間隔はverticalArrangement
で指定しているということになります。
items
のラムダ式はそれ自体がコンポーザブルなので、Row
などを使って複雑なレイアウトを作成できます。ここでは画像と文字列を横に並べて配置しています。Row
のmodifier
引数にfillMaxWitdh()
を追加している点がポイントです。これがないと、文字列の長さによって各アイテムの幅がバラバラになってしまいます。
リストのインデックスを利用する
リストのアイテムの表示に、そのアイテムが何番目のアイテムなのかを表示したいケースがあります。その場合は、items()
の代わりにitemsIndexed()
を使います。itemsIndexed
の場合もitems
と同様にitemContent
引数のラムダ式内にアイテムを描画するコンポーザブルを記述していきます。items
との違いは、ラムダ式にアイテムのインデックスが与えられることです。
さきほどの例に、文字列の先頭にインデックスを追加してみましょう。
@Composable
fun List3(fruits: List<Fruits>) {
LazyColumn( ... ) {
itemsIndexed(fruits) { index, fruit ->
Row( ... ) {
Image( ... )
Text(
text = "$index: ${fruit.name}",
...
)
}
}
}
}
itemsIndexed
直後のラムダ式に与えられる引数が2つになっています。一つ目がインデックスで、2つ目がリストの要素です。Text
にindex
を表示すると、上から順に0, 1, 2, ..と数字が表示されていることが確認できました。
クリックイベントの取得
最後に、アイテムのクリックを検出する方法を確認しておきましょう。
「ボタンクリックでUIを更新する」で説明したように、Jetpack Composeでは任意のコンポーザブルにonClickイベントを設定できます。
@Composable
fun List3(fruits: List<Fruits>, onClickItem: (Int)->Unit = {}) {
LazyColumn( ... ) {
itemsIndexed(fruits) { index, fruit ->
Row(
modifier = Modifier.clickable { onClickItem(index) }
...
) {
Image( ... )
Text( ... )
}
}
}
}
今回はリストのアイテム全体をクリックできるようにしたいので、Row
のmodifier
にclickable
を追加します。必要に応じて、アイテム内に配置した画像やボタンにイベントを設定することもできます。Row
がクリックされたら上位のコンポーザブルにイベントを渡すため、List()
にonClickItem
引数を追加しています。
上位コンポーザブルではonClickItem
を実装して、必要な処理を記述します。今回はダイアログを表示しました。
setContent {
var selectedIndex by remember { mutableStateOf(-1)}
List3(fruits) { selectedIndex = it }
if (selectedIndex >= 0) {
AlertDialog(
onDismissRequest = { selectedIndex = -1 },
confirmButton = {
TextButton(onClick = { selectedIndex = -1 }) {
Text("OK")
}
},
text = { Text("Index $selectedIndex is clicked.")}
)
}
}
List3()
から受け取ったインデックスを、ダイアログに表示できました。ダイアログの使い方はまた今度詳しく説明しますが、選択されたアイテムのインデックスを取得できたことが分かると思います。
まとめ
今回は、アプリを作るうえで欠かせない「リスト」の作り方について説明しました。Jetpack ComposeのLazyコンポーネントを使うと、従来のRecyclerViewに比べてとても見通しの良いソースコードでリストを実現できることが分かっていただけたと思います。クリックイベントの取得も簡単に実現できました。
今回は以上です。
Jetpack Compose入門