Firebase + MVVM + ListAdapterでリアルタイムにリストを更新する

## MVVMとは
Modelはデータの管理や保存、外部との入出力、内部的な処理を担い、Viewは利用者に対する画面表示や入力・操作の受け付けを担当する。両者の間を仲介して互いの状態変更を通知、反映させる役割をViewModelが担う。

アクティビティやフラグメントの中でUIの更新とデータの用意を行うと複雑になってしまう。そこで、データ類の管理を行う処理を「ViewModel」に切り離すことで、アクティビティやフラグメントは画面の表示と操作に専念できる。

# ***1.準備***
## viewBindingを有効にする


```
buildFeatures {
        viewBinding = true
}
```

# ***2.実装***
## データを管理するModelの作成

```kotlin:Item.kt
//コンストラクタではデフォルト値を設定しておく
data class Item(
    var id: String = "",
    val name: String = "",
    val price: Int = 0,
)
```

## 1行分のレイアウトを作成する

```xml:item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:background="#BeE5FC">

    <TextView
        android:id="@+id/tv_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Id" />


    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_id"
        tools:text="Name" />

    <TextView
        android:id="@+id/tv_price"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_name"
        tools:text="Price" />


</androidx.constraintlayout.widget.ConstraintLayout>
```

## ListAdapterを継承したアダプタークラスを作成

```kotlin
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.firebasetestapp.databinding.ItemLayoutBinding

class ItemAdapter() : ListAdapter<Item, ItemAdapter.ItemViewHolder>(DIFF_CALLBACK) {

    class ItemViewHolder(private val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
        //ViewHolderにモデルとViewのデータを紐付けるための関数を作成
        @SuppressLint("SetTextI18n")
        fun bindTo(item: Item) {
            binding.apply {
                tvId.text = "id=${item.id}"
                tvName.text = "name=${item.name}"
                tvPrice.text = "price=${item.price}"
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val binding = ItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ItemViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

    //※ListAdapter側で　submitListをオーバーライドする （受け取ったリストがnullでなければ、リストを元に新しいArrayListのインスタンスを生成して返す仕様に変更）
    //注：submitList(null)が実行されてしまうと以降正しくリストが表示されなくなってしまう。
    override fun submitList(list: List<Item>?) {
        super.submitList(list?.let { ArrayList(it) })
    }

    //DiffUtil.Callbackを実装したオブジェクトを作成し、ListAdapterに渡す
    companion object {
        val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Item>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem == newItem
            }

        }
    }

}
```

### DiffUtil.Callbackとは

２つのリストの差分を計算するときにDiffUtilによって使用されるコールバッククラス
diff = difference（違い）の省略
変更があった際に差分のみ更新される。効率よくリスト表示することができる。

areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
２つの項目が同じデータを持っているかどうかを確認する場合に、DiffUtilによって呼び出される
	
areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
２つのオブジェクトが同じアイテムを表しているかどうかを判断するために、DiffUtilによって呼び出される

★差分があった場合にはAdapterに変更が通知される

## ChildEventListenerからDataSnapshotを受け取るLiveData<DataSnapshot>クラスの作成

```kotlin:FirebaseQueryLiveData.kt
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.lifecycle.LiveData
import com.example.firebasetestapp.source.mvvc.Constants.Companion.LOG_TAG
import com.google.firebase.database.*

class FirebaseQueryLiveData(private val query: Query) : LiveData<DataSnapshot>() {
    constructor(ref: DatabaseReference) : this(query = ref)

    private var listenerRemovePending: Boolean = false
    private val handler: Handler = Handler(Looper.getMainLooper())
    private val listener: MyChildEventListener = MyChildEventListener()
    private val removeListener: Runnable = Runnable {
        query.removeEventListener(listener)
        listenerRemovePending = false
    }

    override fun onActive() {
        if (listenerRemovePending) {
            //2秒経過する前に再度アクティブになった場合は、スケジューリングされた作業を削除し、リスナーがリッスンを継続できるようにする。
            handler.removeCallbacks(removeListener)
        } else {
            query.addChildEventListener(listener)
        }
        listenerRemovePending = false
    }
    //不要なクエリを避けるためLiveDataが非アクティブになった後、Handlerを利用してデータベースリスナーの削除を２秒後にスケジューリングする。
    override fun onInactive() {
        handler.postDelayed(removeListener, 2000)
        listenerRemovePending = true
    }

    //内部にcom.google.firebase.database.ChildEventListenerを継承したクラスを作成
    inner class MyChildEventListener : ChildEventListener {
        override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
            value = snapshot
        }

        override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
            //LiveDataに値をセットする
            value = snapshot
        }

        override fun onChildRemoved(snapshot: DataSnapshot) {
            value = snapshot
        }

        override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
            value = snapshot
        }

        override fun onCancelled(error: DatabaseError) {
            Log.e(LOG_TAG, "Can't listen to query $query", error.toException())
        }

    }

}

```

## LiveDataを保持しておくViewModelクラスの作成

```kotlin:ItemViewModel.kt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class ItemViewModel : ViewModel() {
    //取得したい値が格納されているデータベースの参照をDatabaseReferenceオブジェクトで用意
    companion object {
        val ITEM_REF: DatabaseReference = Firebase.database.reference.child("items")
    }

//ViewModel側では受け取りたいデータ型でLiveDataを作成する
//Transformations.map(LiveData<X> source, Function<X, Y> func)で変換した値を格納する
val liveData: LiveData<Item> = Transformations.map(FirebaseQueryLiveData(ITEM_REF)) {
        it.getValue(Item::class.java)?.apply { id = it.key.toString() }
    }
    val itemList: MutableLiveData<MutableList<Item>> by lazy { MutableLiveData<MutableList<Item>>(mutableListOf()) }
}
```

## FragmentやActivityでLiveDataを監視する処理を実装する

```kotlin:SampleFragment.kt
class SampleFragment : Fragment() {

    companion object {
        fun newInstance() = SampleFragment()
    }

    private var _binding: FragmentSampleBinding? = null
    private val binding get() = _binding!!
    private val viewModel: ItemViewModel by viewModels()
    private val listAdapter: ItemAdapter = ItemAdapter()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        _binding = FragmentSampleBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            rvMain.apply {
                layoutManager = LinearLayoutManager(context)
                adapter = listAdapter
            }
        }

        viewModel.liveData.observe(viewLifecycleOwner) {
            viewModel.itemList.value?.add(it)
            viewModel.itemList.notifyObserver()
        }

        //viewModelのitemList(MutableLiveData<MutableList<Item>>)が変更されるとListAdapterにsubmitListで値が渡される
        viewModel.itemList.observe(viewLifecycleOwner) {
            listAdapter.submitList(it)
        }

    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
    
    //Observerに通知するために実装
    private fun <T> MutableLiveData<T>.notifyObserver() {
        this.value = this.value
    }

}
```

## 3.実行


![Screenshot_20221214-105015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2561089/31712eb0-26d4-d22f-8b12-71f05d35ea63.png)


## Realtime Database
![スクリーンショット 2022-12-14 10.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2561089/12198bcb-0ed8-fe74-9a86-9f595557461f.png)

## まとめ
FirebaseDatabaseにデータが追加されたり、変更があったりするとviewModelのliveData:LiveData<Item>が更新され、FragmentやActivityに通知されます。

それぞれの役割に分かれているので、コードの見通しが良くなりました。
改善の余地がまだまだあると思います。お気づきの点があれば、どんどん教えていただければ幸いです。