🐥

KotlinでのMVVM実装をサクッと理解する

2021/03/02に公開

※2020/05/18 に書いた記事です。
 個人ブログ閉鎖のため、そこそこアクセスのあった記事をZennに転記。

当時手探りで書いた記事なので、Android公式が推奨するMVVMをガチで解説する記事ではないのでご了承ください。

筆者(当時)

  • Android初心者。Kotlinはじめたて。
  • Windowsアプリ開発でMVVMを採用したプロジェクトに数年在籍してた。

対象読者

  • Kotlin、Android初心者。
  • MVVMの話だけ知りたい人
  • 概念の話より、「実際コード書くとこんな感じ」のテイストで読みたい人。

データバインディングと変更通知の上げ方さえわかれば、MVVMはできます。
Kotlinで簡単なMVVMを取り入れてみましょう。

MVVMとは

GUIアプリケーションを「Model-View-ViewModel」の3つに分割して設計、実装するアーキテクチャパターンの一つです。

  • Model:具体的なビジネスロジックを担います。
    • データベース操作や外部通信、データ保持、計算など
  • View:UIの定義と、UIへの入出力を担います。
    • Androidでいうと、layoutのxmlと、ActivityやFragmentにあたります。
  • ViewModel:ViewとModel間で、情報を加工・伝達する役割を担います。

参考:Model View ViewModel - Wikipedia
詳しいことは既に多くの方が記事を書かれているので割愛します。

では順番に説明していきます

Gradleの設定

kaptとデータバインディングを有効にします。
androidx.lifecycle:lifecycle-extensionsのバージョンは、執筆時現在の最新2.2.0を使用します。
2.2.0を使う場合はJava8でないと怒られるので、jvmTarget = '1.8'を設定してあげます。

apply plugin: 'kotlin-kapt'
android {
    dataBinding {
        enabled = true
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
dependencies {
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
    implementation 'androidx.fragment:fragment-ktx:1.2.4'
}

Viewのデータバインディングの設定

ViewModelの作成

Viewに対応した名称のViewModelクラスを新しく作ります。
[Project]ツールウィンドウのコンテキストメニューから、New>Fragment>Fragment(with ViewModel)など、自動でViewModelを作ってくれるものもあります。
なければ新規クラスを作成し、androidx.lifecycleのViewModel()を継承させましょう。

import androidx.lifecycle.ViewModel

class HogeViewModel : ViewModel(){

}

View側でバインディング

まずはxml側。
ポイントは2点。

  • 「data」タグで、バインドする対象のViewModelを指定します。
  • 「data」タグは「layout」の中(「ConstraintLayout」の外)に記載する必要があります。EmptyActivity作成直後は、最上位のタグが「<androidx.constraintlayout.widget.ConstraintLayout>」などとなっていると思いますが、以下のように書き換えましょう。

ビルド時に「AAPT: error: duplicate attribute.」のエラーが発生している場合は、「tools:context="XXX"」をlayoutタグに含めていないか確認しましょう。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="view_model" type="com.example.mvvmsample.viewmodel.MainActivityViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.MainActivity">
        <LinearLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

続いてコード側。
ポイントはコメントに記載しました。
「binding.viewModel」が参照エラーになるという人は、リビルドやAndroidStudioの再起動を試してみてください。

import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProviders
import com.example.mvvmsample.databinding.ActivityMainBinding
import com.example.mvvmsample.viewmodel.MainActivityViewModel

class MainActivity : AppCompatActivity() {

    private val mainActivityViewModel: MainActivityViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // DataBindingを行う場合はDataBindingUtilを使用します。
        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        // ここで、xmlのdataタグで定義したnameが使えるようになります。
        // xml側でスネークケースで定義したnameが、こちら側ではキャメルケースで参照できます。
        // ex)「view_model」->「viewModel」
        binding.viewModel = mainActivityViewModel
    }
}

ViewModelの取得方法は、従来は「ViewModelProviders.of().get()」を利用するのが一般的でしたが、
androidx.lifecycle:lifecycle-extensionsのv2.2.0から「by viewModels()」といった書き方に変わっているため注意してください。

Activityではなく、Fragmentの場合は以下のようになります。

import androidx.databinding.DataBindingUtil
import com.example.mvvmsample.databinding.FragmentHogeBinding
import com.example.mvvmsample.viewmodel.HogeFragmentViewModel

class HogeFragment : Fragment() {

    private val hogeViewModel: HogeFragmentViewModel by viewModels()
    
    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)

        val binding = DataBindingUtil.inflate<FragmentHogeBinding>(inflater, R.layout.hoge, container, false)
        binding.viewModel = hogeViewModel

        return binding.root
    }
}

これでバインディングの準備はOK。

Viewの操作をViewModelで受ける方法

ボタンがポチッと押されたりしたらViewModelで処理を走らせたいですよね。

引数なしの場合

まずはView(xml)側の操作を記述します。
onClickを検知したら、ViewModelのbuttonTapped()をコールするように設定しました。

<Button
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:onClick="@{() -> view_model.buttonTapped()}"/>

そして次にViewModel側。
ボタンタップ時にコールして欲しいメソッド(ここではbuttonTapped())を実装すればOK!

import androidx.lifecycle.ViewModel

class HogeViewModel : ViewModel(){
    fun buttonTapped(){
        // タップ時の処理を書く
    }
}

引数ありの場合

簡単です。普通にメソッドのかっこ内に記述します。
「値ベタ書きは気持ち悪い」
「そもそも引数が値ではなくクラス等のオブジェクトだったらどうすんねん」
という悩みもあると思うので、以下のようにHogeTypeというEnumを渡してみました。

<data>
    <variable name="hogeType" type="com.example.mvvmsample.HogeType" />
</data>

<Button
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:onClick="@{() -> view_model.buttonTapped(hogeType.AAA)}"/>

「data」タグにHogeTypeの参照を追加するのがポイントですね。
一応注意ですが、引数で普通のクラスやインタフェースを渡す場合、インスタンスがNullだったりしたらビルドエラーになるか、アプリが落ちると思います。多分。

Enumやシングルトンやstaticを渡す分には問題ないと思います。

で、Enumクラスの方はこう。

enum class HogeType(val text: String) {
    AAA("aaa"),
    BBB("bbb"),
    CCC("ccc")
}

で、ViewModelの方はこう。

class HogeViewModel : ViewModel(){
    // 引数を持たせる
    fun buttonTapped(hoge: HogeType){
        // タップ時の処理を書く
    }
}

Viewを更新する方法

ViewModel〜Modelで処理した結果をViewに反映させたいですよね。

パターンその1

LiveDataというものを定義し、それをView側で監視します。
概要はこちらが簡潔で分かりやすかったです。
LiveDataメモ

ViewModelにMutableLiveDataを定義し、なんらかのタイミングで更新します。

val hogeLiveData: MutableLiveData<String> by lazy { MutableLiveData<String>() }

fun buttonTapped() {
    hogeLiveData.value = "Tapped!!"
}

LiveDataに値をセットすると、このLiveDataを監視しているやつ(Observer)に通知がいきます。
ViewModelのプロパティ変更を監視したいのはViewですよね。
なので、View側に監視するコードを記述します。

import androidx.lifecycle.Observer

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.viewModel = mainActivityViewModel

        val observer = Observer<String> {
            this.hogeTextView.text = it
        }
        mainActivityViewModel.hogeLiveData.observe(this, observer)
    }
}

監視しているhogeLiveDataに値がセットされると、
「this.hogeTextView.text」に"Tapped!!"を設定するよ、という感じです。

ちなみに、Fragmentの場合は、上記の「observe(this, observer)」を「observe(viewLifecycleOwner, observer)」とします。

パターンその2

実はLiveDataを使用していてハマったことなんですが、、
どうもC#畑で育った人間としては、バインドするプロパティをxml側に書く癖がありまして
パターン1に加えて以下のようにしてしまい、「動かねーじゃねーか!」と思ってました。

<TextView
    android:text="@={view_model.hogeLiveData}" />

循環参照みたいなことになっちゃうのかな?ボタンを押しても永遠に返ってこなくなります。

しかし、上記のようにxml側に直接バインドするプロパティを書きたい場合、以下のようにすると動くことがわかりました。

import androidx.lifecycle.Observer

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.lifecycleOwner = this
        binding.viewModel = mainActivityViewModel
    }
}

「binding.lifecycleOwner = this」という行を足して、パターン1で書いたobserve周りをガッツリ削除してます。
この書き方はとてもスッキリしてて気持ちがいいです。

プロパティの値を更新したいだけであれば、パターン2がおすすめ。
変更通知を受けて、何か他に処理を挟みたいなら、パターン1が良いかと。

別のViewを起動する方法

個人的に、MVVM始めた時に一番悩みがちなテーマだと思います。

  • ViewModelから画面遷移を行いたい。
  • しかしViewModelはViewに依存してはいけないので、遷移先のViewを知らない。

さあどうする?という問題です。
私がみたことがある遷移パターンとしては、

  • ViewModelに、DI(依存性の注入)で自分のViewと遷移先のViewを渡しちゃう。
  • この課題を解決してくれるライブラリのパワーに頼る(WPFならPrismとか)。

大別すると基本この2つです。
で、こんな時は公式のサンプルに頼ると勉強になります。
Googleのarchitecture-samplesでは、
Navigation コンポーネントという物を使って画面遷移しているようです。
余力のある方は以下参考にしてみてください。

https://github.com/android/architecture-samples

https://developer.android.com/guide/navigation

別のViewを更新する方法

例えば、FragmentAのボタンを押して動いた処理の結果を、FragmentBのTextViewに反映させたかったりしますよね。
LiveDataを別クラスに外だしします。

object Hoge {
    val hogeLiveData: MutableLiveData<String> by lazy { MutableLiveData<String>() }
}

で、どこかしらでデータを更新します。

class ResultViewModel : ViewModel() {
    fun buttonTapped() {
        Hoge.hogeLiveData.value = "Uooooo!!!"
    }
}

で、結果を受け取りたい別のFragmentとかでそのLiveDataをバインドすれば反映されます。
※xml側はこれまでの説明と同様、dataタグに追加してください。

class ResultFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
  〜略〜
        binding.data = Hoge
  〜略〜
    }
}

このように、「データを更新するやつ」と「更新がないか監視するやつ」が、互いの存在を意識しなくてすみます。
ユースケースが見えにくくなって複雑化するデメリットはありそう。

余談

MVVMの運用上、本格的に機能を実装してModelが増えていくと、
 ・「Modelのインスタンスは誰が管理するの?」 とか
 ・「Modelを管理する”HogeManager”みたいな神クラスができて肥大化しちまったぜ」 とか
 ・「いくつものViewModelとModelが絡み合って、コード上からユースケースが想像しにくい」
などはあるあるだと思います。
個人的には、Rxとかレイヤーアーキテクチャを取り入れているこの辺りの記事が参考になるな〜と思いました。

何か思ったことがあればコメントいただけると嬉しいです。

Discussion