MVVMとは
Modelはデータの管理や保存、外部との入出力、内部的な処理を担い、Viewは利用者に対する画面表示や入力・操作の受け付けを担当する。両者の間を仲介して互いの状態変更を通知、反映させる役割をViewModelが担う。
アクティビティやフラグメントの中でUIの更新とデータの用意を行うと複雑になってしまう。そこで、データ類の管理を行う処理を「ViewModel」に切り離すことで、アクティビティやフラグメントは画面の表示と操作に専念できる。
1.準備
viewBindingを有効にする
buildFeatures {
viewBinding = true
}
2.実装
データを管理するModelの作成
//コンストラクタではデフォルト値を設定しておく
data class Item(
var id: String = "",
val name: String = "",
val price: Int = 0,
)
1行分のレイアウトを作成する
<?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を継承したアダプタークラスを作成
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とは
2つのリストの差分を計算するときにDiffUtilによって使用されるコールバッククラス
diff = difference(違い)の省略
変更があった際に差分のみ更新される。効率よくリスト表示することができる。
areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
2つの項目が同じデータを持っているかどうかを確認する場合に、DiffUtilによって呼び出される
areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
2つのオブジェクトが同じアイテムを表しているかどうかを判断するために、DiffUtilによって呼び出される
★差分があった場合にはAdapterに変更が通知される
ChildEventListenerからDataSnapshotを受け取るLiveDataクラスの作成
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を利用してデータベースリスナーの削除を2秒後にスケジューリングする。
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クラスの作成
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を監視する処理を実装する
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.実行
Realtime Database
まとめ
FirebaseDatabaseにデータが追加されたり、変更があったりするとviewModelのliveData:LiveDataが更新され、FragmentやActivityに通知されます。
それぞれの役割に分かれているので、コードの見通しが良くなりました。
改善の余地がまだまだあると思います。お気づきの点があれば、どんどん教えていただければ幸いです。