🐷
[Android][kotlin]RecycleViewの + ModelViewテンプレート
Abstruct
RecycleView + ModelViewのソースコードのテンプレートとその説明
概要
RecycleViewってめんどい。RecycleViewに項目追加したのって、スマホ回転で消えてしまうし。
なので実質、ModelViewと絡めて実装する必要があるのでそのテンプレートコードとその説明を残しておきます。
サンプルコード
※おまけのコードがあまりに素晴らしいかったので、それをmainブランチに入れ込んでいる。コンポートネントと役割
コンポーネント | 役割 |
---|---|
MainFragment | UI表示・ユーザー操作の受け口 |
MainViewModel | データ状態の保持と更新ロジック |
CustomAdapter | RecyclerViewの表示ロジック |
fragment_main.xml | UIレイアウト定義 |
技術ポイント
-
データ保持は ViewModel に任せる
- MainViewModel 内で MutableStateFlow<List<String>> を保持し、UI に表示するリストのソースを一元管理。
- Fragment は ViewModel の状態を 監視するだけ にして、UI ロジックとデータロジックを分離。これで画面回転などのライフサイクル変化でもデータが保持される。
-
StateFlow と repeatOnLifecycle を組み合わせて安全に監視
- lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { … } }
を使うことで、Fragment のライフサイクルに合わせて安全に Flow を監視する様にしている。 - 不要なタイミングでコレクションし続けることを防ぎ、リークや無駄な処理を回避。
- lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { … } }
-
RecyclerView.Adapter に updateData メソッドを用意
fun updateData(newData: List<String>) {
data.clear()
data.addAll(newData)
notifyDataSetChanged()
}
- Adapter に 差し替え用のメソッド を用意して、ViewModel から流れてきた新しいデータを反映。
- 今回はシンプルに notifyDataSetChanged() を使っているが、実際のアプリでは DiffUtil を使うと効率的に更新できる。
-
Fragment 側ではイベント(追加・削除)だけを ViewModel に依頼
- 「追加」ボタン → viewModel.addItem()
- 「削除」ボタン → viewModel.removeItem(0)
- Fragment は「どのデータを更新するか」の指示だけ出して、実際のリスト操作はすべて ViewModel に任せる。
👉 この設計により、UI はデータの変化を 受け取るだけ のシンプルな役割になる。
-
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