🐷

[Android][kotlin]RecycleViewの + ModelViewテンプレート

に公開

Abstruct

RecycleView + ModelViewのソースコードのテンプレートとその説明

概要

RecycleViewってめんどい。RecycleViewに項目追加したのって、スマホ回転で消えてしまうし。
なので実質、ModelViewと絡めて実装する必要があるのでそのテンプレートコードとその説明を残しておきます。

サンプルコード
https://github.com/aaaa1597/RecycleViewViewModelSample
※おまけのコードがあまりに素晴らしいかったので、それをmainブランチに入れ込んでいる。

コンポートネントと役割

コンポーネント 役割
MainFragment UI表示・ユーザー操作の受け口
MainViewModel データ状態の保持と更新ロジック
CustomAdapter RecyclerViewの表示ロジック
fragment_main.xml UIレイアウト定義

技術ポイント

  1. データ保持は ViewModel に任せる

    • MainViewModel 内で MutableStateFlow<List<String>> を保持し、UI に表示するリストのソースを一元管理。
    • Fragment は ViewModel の状態を 監視するだけ にして、UI ロジックとデータロジックを分離。これで画面回転などのライフサイクル変化でもデータが保持される。
  2. StateFlow と repeatOnLifecycle を組み合わせて安全に監視

    • lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { … } }
      を使うことで、Fragment のライフサイクルに合わせて安全に Flow を監視する様にしている。
    • 不要なタイミングでコレクションし続けることを防ぎ、リークや無駄な処理を回避。
  3. RecyclerView.Adapter に updateData メソッドを用意

fun updateData(newData: List<String>) {
    data.clear()
    data.addAll(newData)
    notifyDataSetChanged()
}
  • Adapter に 差し替え用のメソッド を用意して、ViewModel から流れてきた新しいデータを反映。
  • 今回はシンプルに notifyDataSetChanged() を使っているが、実際のアプリでは DiffUtil を使うと効率的に更新できる。
  1. Fragment 側ではイベント(追加・削除)だけを ViewModel に依頼

    • 「追加」ボタン → viewModel.addItem()
    • 「削除」ボタン → viewModel.removeItem(0)
    • Fragment は「どのデータを更新するか」の指示だけ出して、実際のリスト操作はすべて ViewModel に任せる。
      👉 この設計により、UI はデータの変化を 受け取るだけ のシンプルな役割になる。
  2. StateFlow.update を使ったイミュータブルなリスト更新

_dataFlow.update { currentList ->
    currentList + item
}
  • update {} ブロック内で 新しいリストを生成して返す ことで、イミュータブルな更新を実現。
    削除時も toMutableList().apply { removeAt(position) } のように新しいリストを返す。
    これによりデータの変更が安全で予測可能になる。

まとめ

  • ViewModel にデータを集約し、UI は監視とイベント通知だけを担当することで責務分離を実現。
  • StateFlow + repeatOnLifecycle を組み合わせることでライフサイクルに強い設計に。
  • Adapter はシンプルな updateData を持たせるだけで、リスト更新を簡潔に実装可能。
  • 実際のプロダクトでは DiffUtil + ListAdapter を使えばより効率的に更新できる。

おまけ

DiffUtil でnotifyDataSetChanged()を使わなくする
このサンプルでは、notifyDataSetChanged()を使ってて、思い切りワーニングがでてる。イケてない。
なので、DiffUtilを使ってRecycleViewの更新を最適化してみた。

CustomAdapterの変更点

+ import androidx.recyclerview.widget.DiffUtil
+ import androidx.recyclerview.widget.ListAdapter
~略~

- class CustomAdapter(private var data: MutableList<String>) :
-         RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {
+ class CustomAdapter :
+     ListAdapter<String, CustomAdapter.CustomViewHolder>(DiffCallback) {

~略~
    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
-        holder.textView.text = data[position]
+        holder.textView.text = getItem(position)
    }

-   override fun getItemCount() = data.size
-
    /* データを更新するメソッド */
-    fun updateData(newData: List<String>) {
-        data.clear()
-        data.addAll(newData)
-        notifyDataSetChanged()
-    }

+    companion object {
+        private val DiffCallback = object : DiffUtil.ItemCallback<String>() {
+            override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+                // アイテムが一意に識別できるなら ID 比較を使う
+                return oldItem == newItem
+            }
+
+            override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+                // 中身が同じかどうか
+                return oldItem == newItem
+            }
+        }
+    }

MainFragmentの変更点

~略~
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
-        /* StateFlow監視 */
-        dataFlowJob = lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) {
-            _viewModel.dataFlow.collect { newData ->
-                (_binding.ryvSample.adapter as CustomAdapter).updateData(newData)
-            }
-        }}
- onCreate()で定義してた dataFlow.collect を onViewCreated() に移動
    }
~略~
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
~略~
-        val adapter = CustomAdapter(mutableListOf())
+        val adapter = CustomAdapter()
~略~
+        /* StateFlow監視 */
+        dataFlowJob = viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) {
+            _viewModel.dataFlow.collect { newData ->
+                adapter.submitList(newData)
+            }
+        }}

おまけの技術ポイント(DiffUtil 版)

  • ListAdapter を使ったら notifyDataSetChanged() を呼ばなくてもよくなった。
  • 差分計算を 自動でやってくれる。リストの一部だけを効率的に更新できる。
  • UI のちらつきが減り、大規模データでもパフォーマンスが向上。
  • コードの修正も adapter.submitList(newData) で済むのでシンプル。

Discussion