Jetpack Compose入門(17) リスト

この記事をシェア


さて今回はリストについて説明していきます。リストは、同じ見た目の項目を複数並べて表示するためのUIです。設定画面や連絡先の表示、ファイルやコンテンツの一覧表示など、あらゆる場面で使います。従来はRecyclerViewで実現していましたが、正直、かなり面倒でしたし、初心者にはとっつきにくくもありました。しかしJetpack Composeならとても簡単に実装できるので、ぜひ試してみてください。

Lazyコンポーザブル

Jetpack Composeのリストは、 androidx.compose.foundation.lazyパッケージに定義されているLazyコンポーザブルを使って実現します。アイテムを縦に並べる場合は LazyColumn()、横に並べる場合はLazyRow()、項目を格子状に並べる場合はLazyVerticalGrid()を使います。なおLazyVerticalGrid()は2022年1月時点ではExperimentalFoundationApiなので、今後仕様変更などがあるかもしれません。

コンポーネントを配置する」で説明したColumnRowとの違いは、画面描画時にすべてのアイテムを描画するかどうかです。ColumnLowは画面初期化時にすべてのアイテムを描画するのに対して、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のラムダ式では、リストに表示するアイテムを定義します。アイテムがListArrayで保持されている場合は、items()を使います。items()の定義は以下の通りです。

inline fun <T : Any?> LazyListScope?.items(
    items: List<T?>?,
    noinline key: ((item) -> Any)? = null,
    crossinline itemContent: (@Composable @ExtensionFunctionType LazyItemScope.(item) -> Unit)?
): Unit

引数itemListを渡し、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,
                )
            }
        }
    }
}

LazyColumnverticalArrangement引数では、Arrangement.spacedBy()を使ってリストアイテム同士の間隔を指定しています。contentPadding引数では、PaddingValues()を使ってアイテムの周囲の余白を指定しています。左右の余白とAppleの上の余白はcontentPaddingで、AppleとOrangeの間などのアイテム間隔はverticalArrangementで指定しているということになります。

itemsのラムダ式はそれ自体がコンポーザブルなので、Rowなどを使って複雑なレイアウトを作成できます。ここでは画像と文字列を横に並べて配置しています。Rowmodifier引数に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つ目がリストの要素です。Textindexを表示すると、上から順に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( ... )
            }
        }
    }
}

今回はリストのアイテム全体をクリックできるようにしたいので、Rowmodifierclickableを追加します。必要に応じて、アイテム内に配置した画像やボタンにイベントを設定することもできます。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入門


この記事をシェア