LazyColumnは「複数のデータやオブジェクト」または「コレクション」を一覧(リスト)表示するUIです。
単純に一覧表示できれば良い用途もありますが、表示に加えてアイテムの制御(変更/追加/移動/削除)を必要とする場面が多くあります。
最近の携帯端末はパソコンと同じ利便性をアプリに求めますから、「データの表示だけでなく編集もできる」という機能は必須の要望です。
ここでは、LazyColumnでアイテムの制御を行う方法を紹介します。
※環境:Android Studio Koala | 2024.1.1
Kotlin 1.9.0
Compose Compiler 1.5.1
androidx.compose.foundation:foundation 1.6.8
目次
アイテムの制御
LazyColumnで作成した一覧表示に、次のようなアイテムの制御機能を組み込む場合を考えます。
- (a)アイテムの変更(Change)
- (b)アイテムの追加(Insert)
- (c)アイテムの移動(Move)
- (d)アイテムの削除(Remove)
ユーザへ制御の結果を知らせるためには、表示の更新が必要です。
画面の描画
Jetpack Composeにおけるアプリ画面の描画は、3つのフェーズ(Composition/Layout/Drawing)からなる描画処理により行われます。
ただし、3つのフェーズが毎回実行されることは無く、表示(状態)に変更があったUI要素(Composable関数)の必要なフェーズのみを実行します。
これにより、アプリ画面の描画に必要なコストを、大幅に削減しています。
※詳細は「Jetpack Compose:Composeによるアプリ画面の描画」を参照
※詳細は「Jetpack Compose:再Composeとスキップ」を参照
表示の更新
LazyColumnを使って一覧表示するだけであれば、以下のように記述するだけです。ただし、アイテムの変更はできません。
@Stable data class NumData(val id: Int, var num: Int) @Composable fun SampleList(datas: List<NumData>) { LazyColumn { items(items = datas) { ItemBox { Text(text = "Item %02d".format(it.num)) } } } }
アイテムの変更と表示の更新を行えるようにするには、次の処理が必要です。
- (1)要素の変更が可能なコレクションを作成(List⇒MutableListへ変換)
- (2)再Composeを超えて要素を保持(rememberでComposition内に格納)
- (3)要素の変更を監視、変更で再Compose(mutableStateListOf)
- (4)要素の変更を行う仕組み(Buttonなどのイベント処理)
※mutableStateOfについては「Jetpack Compose:再Composeのスケジューリング」を参照
例えば、「上から5番目にアイテムを追加」を実装すると、次のようになります。
@Stable data class NumData(val id: Int, var num: Int) private var ID: Int = 100 private fun newData() = try { NumData(ID, ID) } finally { ID++ } @Composable fun SampleList(datas: List<NumData>) { val _datas = remember { // (2) mutableStateListOf<NumData>().apply { addAll(datas) } // (1),(3) } LazyColumn { items(items = _datas) { ItemBox { Text(text = "Item %02d".format(it.num)) } } } Button( onClick = { _datas.add(4, newData()) } // (4) ) { Text(text = "ins")} }
Jetpack Composeは「状態の変更を検出して再Composeさせる」という、表示を更新するための御膳立てが難しいです。しかし、mutableStateListOfが行ってくれます。とっても便利です。
更新の効率化
LazyColumnによる一覧表示は巨大なUIツリーです。「アイテムを構成するUI要素 × アイテム数」になるからです。
多数のUI要素(Composable関数)を一度に扱うため、重い処理になります。
「重い処理」は端末のシームレスな動作を阻害します。ですので、更新の効率化が必須です。
不要な再Compose
先に示したサンプル(「上から5番目にアイテムを追加」)は、図のような再Composeが行われます。
item04~08のアイテムは表示が変わっていないのに、再Composeが行われています。これは不要な再Composeです。
アイテムをポジションで識別(効率化前)
不要な再Composeが行われる原因は、LazyColumnがアイテムをポジション(並び順)で識別しているからです。
追加されたアイテムによるポジションのずれを、状態の変化として扱います。
- 4番目のアイテムの状態が4→100に変化―>再Compose
- 5番目のアイテムの状態が5→ 4に変化―>再Compose
- :
- 9番目のアイテムの状態が9→ 8に変化―>再Compose
アイテムをキーで識別(効率化後)
アイテムにキーを登録することができます。
キーは同一LazyColumn内でアイテム毎にユニーク(重複は許されない)なオブジェクトです。IDのような数値で構いません。
@Stable data class NumData(val id: Int, var num: Int) private var ID: Int = 100 private fun newData() = try { NumData(ID, ID) } finally { ID++ } @Composable private fun SampleList(datas: List<NumData>) { val _datas = remember { // (2) mutableStateListOf<NumData>().apply { addAll(datas) } // (1),(3) } LazyColumn { items(items = _datas, key = { it.id }) { // NumData#idをキーにした ItemBox { Text(text = "Item %02d".format(it.num)) } } } Button( onClick = { _datas.add(4, newData()) } // (4) ) { Text(text = "ins")} }
キーを登録すると、LazyColumnがアイテムをキーで識別するようになります。
追加されたアイテムがあったとしても、「Key:4」により識別されるアイテムの状態は変わりません。
状態の変化が無くなるので再Composeは行われませんが、位置が変化するのでLayoutフェーズで配置が変わります。
制御の例
以下はアイテムの制御の例です。
@Stable data class NumData(val id: Int, var num: Int) private var ID: Int = 100 private fun newData() = try { NumData(ID, ID) } finally { ID++ } @Composable private fun ItemCtrl(datas: List<NumData>) { val _datas = remember { mutableStateListOf<NumData>().apply { addAll(datas) } } Column(modifier = Modifier.fillMaxSize().padding(2.dp)) { Row { Button(modifier = Modifier.weight(1.0f), onClick = { _datas.set(4, newData()) } ) { Text(text = "chg")} Button(modifier = Modifier.weight(1.0f), onClick = { _datas.removeAt(4) } ) { Text(text = "rmv")} Button(modifier = Modifier.weight(1.0f), onClick = { _datas.add(4, newData()) } ) { Text(text = "ins")} Button(modifier = Modifier.weight(1.0f), onClick = { val _temp = _datas.get(4) _datas.removeAt(4) _datas.add(2, _temp) } ) { Text(text = "mov")} } LazyColumn { items(items = _datas, key = { it.id }) { ItemBox { Text(text = "Item %02d".format(it.num)) } } } } }
Change(変更)
5番目のアイテムを新しいアイテムに変更します。
Insert(追加)
5番目に新しいアイテムを追加します。
Move(移動)
5番目のアイテムを3番目に移動します。
Remove(削除)
5番目のアイテムを削除します。
関連記事: