【Android Kotlin】MVVM+DataBinding+LiveData+ Coroutine+Flow サンプル
Android, Androidstudio, Kotlin, PC, Realm, Windows, 紹介
前回、DataBindingについて解説させていただきましたが、その際にfindViewByIdよりもDataBindingが推奨されているという内容を記載しました。
それと同じように、使っていた技術よりも推奨されるものが出てきたり、新しい考え方であったり、アプリ開発をより効率的に行えるような、便利な機能がどんどん登場しています。
そこで今回は、以前作成したアプリを便利な機能を使って改良し、そこで使ったいくつかの機能について、それぞれ解説していきたいと思います。
目次
アプリの変更点
今回は、前々回のブログで紹介したRealmにデータを保存してそれを表示するアプリに修正を加えたものを使って解説しますので、まずはどのよう変更点があるかコードを見ながら説明します。
修正前のアプリの詳しい内容についてはブログで紹介していますので、まだ見ていないという方はそちらもご覧ください。
機能の追加
アプリの機能自体は修正前とほとんど変わらず、データをrealmに保存して、保存した内容を表示するものですが、新しい機能として「保存」ボタンを押した後に入力したテキストを消去するかどうかの選択肢を表示するようにしました。
MainActivity.kt
まずはMainActivityの変更点から紹介します。
大きく変わった点として、findViewByIdを使用していた箇所をDataBindingに変えてレイアウトと連携させています。
加えて、今回新しく追加した「保存」ボタンを押した後の表示といった各種イベントの登録を行っています。
package com.example.sample_realm import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.example.sample_realm.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val viewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // レイアウトファイルへ // viewModel(this@MainActivity.viewModel)と // lifecycleOwner(this@MainActivity)を // (binding = it)連携させる ActivityMainBinding.inflate(layoutInflater).apply { viewModel = this@MainActivity.viewModel lifecycleOwner = this@MainActivity }.let { binding = it viewModel.binding = it setContentView(it.root) // アダプターインスタンス作成 viewModel.sampleAdapter = SampleListAdapter( inflater = layoutInflater ) // レイアウトファイルのid/sampleListへ連携させる it.sampleList.adapter = viewModel.sampleAdapter } // Realのイベント設定 viewModel.setRealmEvents() // layoutファイル 各種イベント登録 collectFlow() } // layoutファイル 各種イベント登録 private fun collectFlow() { viewModel.apply { lifecycleScope.launchWhenStarted { clickWriteFlow.collect { AlertDialog.Builder(this@MainActivity) .setCancelable(false) .setMessage("正しく保存できました\n入力欄を初期化しますか?") .setNegativeButton("しない") { _, _ -> // No処理 } .setPositiveButton("する") { _, _ -> // Yes処理 liveDataTextLd.value = "" } .show() } } } } }
SampleListAdapter.kt
ListAdapterで大きな変更点は特にありませんが、以前はこの中にあったViewHolderがなくなって、新しくViewHolderのクラスを作って独立させています。
package com.example.sample_realm import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.example.sample_realm.databinding.ItemSampleBinding class SampleListAdapter( private val inflater: LayoutInflater ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val sampleList = mutableListOf<String>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder = SampleViewHolder.create(inflater, parent, false) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as SampleViewHolder).bind(sampleList[position]) } override fun getItemCount(): Int = sampleList.size fun updateSampleList(sampleList: List<String>) { // 一度クリアしてから新しいメモに入れ替える this.sampleList.clear() this.sampleList.addAll(sampleList) // データに変更があったことをadapterに通知 notifyDataSetChanged() } }
SampleViewHolder.kt
ViewHolderは先述の通り、ListAdapterの中に含めていたものを新しいクラスとして作成しています。
記述してある処理自体は大きく変わっていませんが、DataBindingに合わせて記述の仕方が変わっています。
package com.example.sample_realm import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.example.sample_realm.databinding.ItemSampleBinding class SampleViewHolder private constructor( private val binding: ItemSampleBinding ) : RecyclerView.ViewHolder(binding.root) { companion object { fun create( inflater: LayoutInflater, parent: ViewGroup, attachToRoot: Boolean ) = SampleViewHolder( ItemSampleBinding.inflate( inflater, parent, attachToRoot ) ) } fun bind( sample: String ) { binding.sampleTextView.text = sample } }
SampleViewModel.kt
修正にあたって、新しく作成されたクラスになります。後述するMVVMの設計モデルに則ってRealm関係の処理やクリックした際の処理を記述しています。また、Coroutineとしての処理を記載し、API通信などを追加できるようにしたり、変数に値をセットしたりしています。
package com.example.sample_realm import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.sample_realm.databinding.ActivityMainBinding import io.realm.Realm import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch class SampleViewModel: ViewModel() { lateinit var binding: ActivityMainBinding lateinit var sampleAdapter: SampleListAdapter private val realm = Realm.getDefaultInstance() // LdプレフィックスでLiveDataと分かるように記載 val liveDataTextLd = MutableLiveData<String>() // FlowプレフィックスでSharedFlowと分かるように記載 val clickWriteFlow = MutableSharedFlow<Unit>() // イニシャライズ init { initInputInfoData() } private fun initInputInfoData() { // コルーチンとして処理開始 viewModelScope.launch { runCatching { // API 通信などここでTry }.onSuccess { // set data liveDataTextLd.value = "" }.onFailure { // エラー }.also { // 後処理 } } } // 書込みボタンクリックイベント fun onClickWrite() { val text = liveDataTextLd.value.toString() //テキストが空の場合には無視をする // if (value.isEmpty()) return if (text.isEmpty()) return viewModelScope.launch { // putData(liveDataTextLd.value.toString()) putData(text) clickWriteFlow.emit(value = Unit) } } private fun putData(value: String) { //テキストが空の場合には無視をする // if (value.isEmpty()) return // Realmのトランザクション realm.executeTransactionAsync { //DataListのオブジェクト作成 val data = it.createObject(DataList::class.java) //nameに先ほど入力されたtextを入れる data.name = value //データの上書きをする it.copyFromRealm(data) } } fun setRealmEvents() { // DBに変更があった時に通知が来る realm.addChangeListener { it -> //変更があった時にリストをアップデートする val list = it.where(DataList::class.java).findAll().map { it.name } //UIスレッドで更新する binding.sampleList.post { sampleAdapter.updateSampleList(list) } } // 初回表示の時にメモ一覧を表示 realm.executeTransactionAsync { val list = it.where(DataList::class.java).findAll().map { it.name } // UIスレッドで更新する binding.sampleList.post { sampleAdapter.updateSampleList(list) } } } }
activity_main.xml
DataBindingをつかってViewModelと結びついています。これによって、EditTextに入力されたデータがViewModelのLiveDataでも使用可能になったり、ViewModelのメソッドにアクセスできるようになっています。
<?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> <variable name="viewModel" type="com.example.sample_realm.SampleViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <EditText android:id="@+id/sample_edit_text" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="テキストを入力してください" android:text="@={viewModel.liveDataTextLd}" android:textColor="#000000" app:layout_constraintEnd_toStartOf="@id/add_button" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/add_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="保存" android:onClick="@{() -> viewModel.onClickWrite()}" android:textColor="#000000" android:textSize="20sp" app:iconTint="#000000" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/sampleList" android:layout_width="0dp" android:layout_height="0dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/sample_edit_text" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
使った機能や設計モデルの紹介
コードを見ながらどのような変更があったのか、というのを簡単に説明しましたので、続いてはそこで使った機能や設計モデルについて詳しく紹介していきたいと思います。
MVVM
ViewModelの説明にあったように、MVVMの設計モデルを使った形に変えています。
MVVMはModel, View, ViewModelの頭文字をとったもので、MVCから派生した設計モデルになります。
MVCと同じ部分もありますが、MVVMのそれぞれについて解説していきます。
Model
Modelに関しては、MVCと同じくシステムの中のビジネスロジックを担当している部分になります。データの処理を行って、データの変更をViewに通知したりします。
View
ViewもMVCと同じ役割で、Modelが扱っているデータを取り出して、UIへの出力などを行っています。
View Model
ViewModelはViewとModel間の伝達や、状態の保持を担当します。
今回のサンプルでは、新しくViewModelのクラスを作成し、それぞれとデータのやり取りを行っています。
DataBinding, LiveData
DataBindingは前回のブログで紹介しましたが、その際は外部からViewの表示を変えることができるという単方向のみの説明でした。しかし、DataBindingには双方向でのデータのやり取りをする機能もあります。
双方向の場合は例にあるように “@={viewModel.liveDataTextLd}” という様に@の次に “=” を入力する必要があります。これによって、Viewからもデータを送るということが可能になります。
また、その際に LiveData というデータを監視するクラスを使用することで、テキスト入力欄に変更があるたびに、自動でデータの更新を行ってくれます。
Coroutine
Coroutineは非同期処理を簡略化することができるデザインパターンのことです。
今回の例では、非同期処理をする必要がほとんどありませんでしたが、コードを紹介していた時にも触れたように、メインの機能と合わせて、API通信をする時等に使用すると非常に役立つものになっています。
Flow
Flowは先ほど紹介した Coroutine の一種であり、サンプルでは MutableSharedFlow として実装されていたものになります。今まで使われていたRxと同じような動きをすることが可能で、複数の値を順次出力できるため、データベースからリアルタイムで更新情報を受け取る際などに便利な機能です。
サンプルではクリックされた際に、MainActivityのイベント登録処理にデータを渡しています。
まとめ
いかがでしたでしょうか。説明を読んだだけでは理解が難しい部分もあると思いますが、自分でそれぞれの機能を使ってみるとより理解が深まりますので、アプリ開発の際にはぜひ活用してみてください。