デジタルの力で仕事効率アップをサポート。お客様の業務を加速させる。

【Android Kotlin】MVVM+DataBinding+LiveData+ Coroutine+Flow サンプル

Android, Androidstudio, Kotlin, PC, Realm, Windows, 紹介

前回、DataBindingについて解説させていただきましたが、その際にfindViewByIdよりもDataBindingが推奨されているという内容を記載しました。
それと同じように、使っていた技術よりも推奨されるものが出てきたり、新しい考え方であったり、アプリ開発をより効率的に行えるような、便利な機能がどんどん登場しています。
そこで今回は、以前作成したアプリを便利な機能を使って改良し、そこで使ったいくつかの機能について、それぞれ解説していきたいと思います。


アプリの変更点

今回は、前々回のブログで紹介したRealmにデータを保存してそれを表示するアプリに修正を加えたものを使って解説しますので、まずはどのよう変更点があるかコードを見ながら説明します。

修正前のアプリの詳しい内容についてはブログで紹介していますので、まだ見ていないという方はそちらもご覧ください。

https://ictdoctor.jp/%e3%80%90kotlin%e3%80%91realm%e3%81%ab%e3%83%87%e3%83%bc%e3%82%bf%e3%82%92%e4%bf%9d%e5%ad%98%e3%81%99%e3%82%8b%e6%96%b9%e6%b3%95%e3%82%92%e8%a7%a3%e8%aa%ac%ef%bc%81%e3%80%90realm%e3%80%91/

機能の追加

アプリの機能自体は修正前とほとんど変わらず、データを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のイベント登録処理にデータを渡しています。


まとめ

いかがでしたでしょうか。説明を読んだだけでは理解が難しい部分もあると思いますが、自分でそれぞれの機能を使ってみるとより理解が深まりますので、アプリ開発の際にはぜひ活用してみてください。