飴屋

Kotlin/さらばRecyclerView

さて、RecyclerViewへの愚痴を始めましょうか。RecyclerViewは同じようなViewを大量に並べるのによく使ってました。俗にいう一覧画面なんかはWebでもアプリでもよく出てきそうなもんです。ただAndroidアプリでは、とにかく大量のデータを表示しなければならない(かもしれない)ので、メモリの消費やパフォーマンスの観点から、一覧表一つ作るのにも、細かく最適化できるような仕組み(= RecyclerView)が用意されていたんだと思います。最近、実装してなかったのでうろ覚えですが、まず、RecyclerViewと表示データをつなぐような役割のアダプターが必要でしたね。(RecyclerView.Adapter)それから一覧表示される各アイテムの状態を表すようなItemDetailsなんてのも作りましたね。各アイテムは画面によって違うのでそれぞれカスタマイズされたものを用意するわけです。表の中身が更新されたときなんかもいちいちnotifyして画面の更新タイミングを伝えなきゃいけないし、なんか似たようなちょっと違うものを何度も作り続けなきゃいけないのが地味に苦痛でした。きっともっと楽な方法があるんだろうなと思いつつ、むやみに一覧表を作り続けてきたのでした。

今回、それを全部Composableな関数に書き換えてやることにしました。

if (mainViewModel.itemSearching.value) {
    Text(
        text = "アイテムを探してる最中です。"
    )
} else {
    LazyColumn {
        // ...
    )
}

まず、一覧表の前に実装したのが、「表に表示されるアイテムは揃っているかどうか」という点についてですね。揃ってるかどうかを状態としてViewModelの中で持たせたので、それを参考に「揃ってなかった場合の表示」も出せるように処理を分岐させます。いや、アプリに載せる広告の掲載業者さんが「画面に何も表示されないタイミングがある!」ってことで広告の配信を停止してくることがあったので気を付けているのですが、そもそもユーザーにとっても不親切なので、大事なポイントなんでしょうね。表示件数が0件のときなんかも気を付けたいですね。

var uniqueKey = mainViewModel.uniqueKey
var selectedKey by remember { mutableStateOf(uniqueKey) }
LazyColumn {
    items(mainViewModel.itemList.value.count()) { i ->
        Card(
            modifier = Modifier.fillMaxWidth().padding(8.dp),
            colors = CardDefaults.cardColors(
                containerColor = if (mainViewModel.itemList.value[i].uniqueKey == selectedKey) Color(0xFFE3F2FD) else Color.White
            ),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
            onClick = {
                selectedKey = mainViewModel.itemList.value[i].uniqueKey
                uniqueKey = mainViewModel.itemList.value[i].uniqueKey
            }
        ) {
            Text(
                text = mainViewModel.itemList.value[i].name,
                fontWeight = if (mainViewModel.itemList.value[i].uniqueKey == selectedKey) FontWeight.Bold else FontWeight.Normal,
                modifier = Modifier.padding(8.dp)
            )
        }
    }
}

続いて、脱RecyclerViewの肝ともいえるLazyColumn を書いていきましょう。Columnと違って遅れて(Lazy)中身が決定される一覧構成要素という認識です。動的なダイナミックなColumnって感じでしょうか。中身の変更を受けて画面が更新されるところが、これまで学んできたComposableな関数によるレイアウトのいいところですね。配列の中身が変更する度に、画面と配列データの同期を自分でとっていたあの頃とはオサラバです。

今回の一覧表の中身はViewModelにitemListという名前でリストを用意しました。mutableStateOf(emptyList<A>()) みたいな感じで空のリストの状態という形で持っています。Aという型はuniqueKeyという一意のキーとnameという名前を表す文字列をプロパティに持たせてあります。名前は一覧表にTextで表示されます。Textの親はCardというComposableパーツで、まぁ、いろんなパーツをひとまとめに括るのに丁度良いやつです。型A一つにつき一枚のカードが増えるわけですね。物で例えられると理解が進みやすくていいですね。

LazyColumn はitemsというループを構成できるらしく、ここに先のリスト(itemList)を渡すと、その中身の数の分、ループが回ってCardが揃います。

CardにはonClick をつけてみました。クリック(タップ)されたときにアイテムのuniqueKeyを記録します。記録先は冒頭でrememberしたselectedKey です。これが記録(更新)されたことをトリガーに一覧表が更新され、selectedKey と各アイテムのuniqueKey が比較され、一致したら

  • Cardの背景色を選択色に
  • 名前のテキスト部分のフォントを太字に

と表示が変更されるようにしました。クリックで何が選択されているかは記録されているので、何かを選択するUIがこれだけで完成しました。表示項目もこのComposableな関数を変更して自由に拡張できますね。RecyclerViewのときはCardにあたる中身を表すレイアウトXMLをファイルツリーから探してこなくちゃでしたね~。あっちこっちのファイルをちょこちょこ直して回るの、性に合わなくて嫌いだったんですよね。Composableな関数はRecyclerViewを捨てられたことが一番うれしいかもしれません、今のところ。

とはいえ、今まで書き散らしてきたRecyclerViewがいっぱいあるし、それぞれ微妙に違うカスタマイズが入っていたりするので、しばらくは立ち止まりながら書き換えていくことになりそうです。

一覧へ