ViewModelの実装方法まとめ

前回は`Navigation`に関しての記事を書きましたが、今回は`kotlin`における`ViewModel`の実装方法、`LiveData`の使い方、`DataBinding`の適用方法についてまとめていきます。

# この記事の内容
公式の[ドキュメント](https://developer.android.com/guide/navigation?hl=ja)やトレーニング「[Android Kotlin の基礎](https://developer.android.com/courses/kotlin-android-fundamentals/overview?hl=ja)」のレッスン5「アーキテクチャ コンポーネント」を参考に実装のポイントとエッセンスをまとめていきます。
`ViewModel`とは何か、また`MVVM`デザインパターン等については触れません。

### 前提知識

* kotlinの基礎的な文法
* AndroidStudioの使い方／アプリの作り方
* 画面や画面部品の配置方法
* `Navigation`の実装方法（前回記事参照）

### 開発環境

* Windows 10 Home
* Android Studio 4.2.1

# 作成するサンプル
_**StartFragment**_ 、 _**SecondFragment**_ 、 _**FinalFragment**_ の3つの画面を持ち、遷移ボタンでそれぞれの画面に遷移します。
_**SecondFragment**_ には _**AddButton**_ 、 _**SubtractButton**_ の2種類のボタンがあり、これをクリックすることで画面上の**score**が加算／減算されます。
このクリック時の処理（**score**の更新、画面遷移）を`ViewModel`により置き換えていきます。
リファクタリング前後で画面の動きは変わりません。

![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1073313/9b287e86-791b-d556-4aab-b9dda9527348.gif)

# GetStarted
それではさっそく作業を始めましょう。

## build.gradle(app)の更新
_**build.gradle(app)**_ ファイルを開き、`dependencies`ブロックに以下の通り`implementation`を追記してください。

```gradle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
```



## ViewModelの作成

各`Fragment`に対し`ViewModel`を作成します。 _**java**_ フォルダに _**Kotlin Class**_ を追加してください。
クラス名は _**StartViewModel**_ 等が分かりやすいでしょう。
作成したクラスは`ViewModel()`を継承させ、`init`ブロックを追加します。

```kotlin
class SecondViewModel : ViewModel() {
    init {
    }
}
```

_**SecondFragment**_ のメンバ変数として`ViewModel`を定義します。
`ViewModel`は`onCreateView()`メソッドで生成します。

```kotlin
class SecondFragment : Fragment() {

    private var _score = 1

    private lateinit var viewModel: SecondViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // (略)
        viewModel = ViewModelProvider(this).get(SecondViewModel::class.java)
        // (略)
    }
}
```

## ViewModelの実装
_**SecondFragment**_ では画面に描画するスコアの変数を`Fragment`内部で保持していますが、これを _**SecondViewModel**_ に移し替えます。
同時**AddButton**、**SubtractButton**がクリックされた際の処理をメソッドとして定義しておきます。

```kotlin
class SecondViewModel : ViewModel() {

    var score = 0
    var displayScore = ""

    init {
    }

    fun addScore() {
        score++
        displayScore = score.toString()
    }

    fun subtractScore() {
        score++
        displayScore = score.toString()
    }
}
```

あわせて _**SecondFragment**_ 側もリファクタリングします。
`Binding`オブジェクトの変数に`ViewModel`の値を設定している点に注目してください。

```kotlin
class SecondFragment : Fragment() {

    private var _score = 1

    private lateinit var binding: FragmentSecondBinding
    private lateinit var viewModel: SecondViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_second,
            container,
            false
        )

        // viewModelの生成
        viewModel = ViewModelProvider(this).get(SecondViewModel::class.java)

        // クリックリスナを設定
        binding.toFinalFragmentButton.setOnClickListener { view: View ->
            view.findNavController().navigate(
                SecondFragmentDirections.actionSecondFragmentToFinalFragment(_score)
            )
        }
        binding.addButton.setOnClickListener {
            viewModel.addScore()
            _score = viewModel.score
            binding.scoreText.text = viewModel.displayScore
        }
        binding.subtractButton.setOnClickListener {
            viewModel.subtractScore()
            _score = viewModel.score
            binding.scoreText.text = viewModel.displayScore
        }

        return binding.root
    }
}
```

## ViewModelFactoryの作成
これは必ずしもマストな作業ではないのですが、`ViewModel`を生成する`ViewModelFactory`を作成しておきます。
`factory method`は[Creational Pattern](https://en.wikipedia.org/wiki/Creational_pattern)の一種だそうです（スミマセンこの辺りはおれも勉強不足）。
ここでは _**FinalFragment**_ の`ViewModel`と`ViewModelFactory`を新たに作成します。
_**FinalViewModel**_ はこんな感じ。

```kotlin
class FinalViewModel(finalScore: Int) : ViewModel() {
    var score = finalScore
    init {
    }
}
```

_**FinalViewModelFactory**_ はこう。`Factory`クラスは`create()`メソッドを定義する必要があります。

```kotlin
class FinalViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(FinalViewModel::class.java)) {
            return FinalViewModel(finalScore) as T
        }
        throw IllegalArgumentException("不正なViewModelクラスが引数に設定されました。")
    }
}
```

_**FinalFragment**_ も下記の通りリファクタリングします。
`ViewModel`の生成方法が前節で紹介したものと異なる点に注意してください。

```kotlin
class FinalFragment : Fragment() {

    private lateinit var binding: FragmentFinalBinding
    private lateinit var viewModel: FinalViewModel
    private lateinit var viewModelFactory: FinalViewModelFactory

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_final,
            container,
            false
        )

        // 引数
        val args = FinalFragmentArgs.fromBundle(requireArguments())

        // viewModelFactoryの生成
        viewModelFactory = FinalViewModelFactory(args.argsScore)

        // viewModelの生成
        viewModel = ViewModelProvider(this, viewModelFactory)
            .get(FinalViewModel::class.java)

        // 設定
        binding.finalScoreText.text = viewModel.score.toString()

        return binding.root
    }
}
```

# LiveData
## LiveDataの導入
`ViewModel`のメンバ変数を`LiveData`に置き換えます。
`LiveData`を活用することでこれまで明示的に設定していた画面上の**score**が、値の更新と同時に自動的に置き換わります。
_**SecondViewModel**_ を下記の通りリファクタリングします。

```kotlin
class SecondViewModel : ViewModel() {

    var score = MutableLiveData<Int>()
    var displayScore = MutableLiveData<String>()

    init {
        score.value = 0
        displayScore.value = ""
    }

    fun addScore() {
        score.value = (score.value)?.plus(1)
        displayScore.value = score.value.toString()
    }

    fun subtractScore() {
        score.value = (score.value)?.minus(1)
        displayScore.value = score.value.toString()
    }
}
```

_**SecondFragment**_ のリスナはこんな感じ。

```kotlin
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // (略)
        binding.addButton.setOnClickListener {
            viewModel.addScore()
            _score = viewModel.score.value?:0
            binding.scoreText.text = viewModel.displayScore.value
        }
        binding.subtractButton.setOnClickListener {
            viewModel.subtractScore()
            _score = viewModel.score.value?:0
            binding.scoreText.text = viewModel.displayScore.value
        }
        // (略)
    }
}

```

## Observerの登録
`LiveData`の状態を監視する`Observer`を実装していきます。
_**SecondFragment**_ の`ViewModel`生成処理の直下に、以下のコードを追記してください。

```kotlin
        // Observerの登録
        viewModel.displayScore.observe(viewLifecycleOwner, Observer { newDisplayScore ->
            binding.scoreText.text = newDisplayScore
        })
```

これによりクリックリスナの`binding.scoreText.text = viewModel.displayScore.value`という記述は不要となるため、削除します。

アプリを起動してみてください。
`Fragment`側で明示的に設定していた**socre**が、自動で更新されるようになりました。

## Observerのカプセル化
デザインパターンとして、`ViewModel`で保持する変数は基本的に`ViewModel`内部でしか必要ありません。
つまり`ViewModel`では**read only**の変数のみを**public**として定義すればより安全な設計となるため、カプセル化の検討を行います。
_**SecondViewModel**_で保持する**socre**、**displayScore**を下記の通り**private**な`MutableLiveData`、**public**な`LiveData`に分離します。
あわせて`ViewModel`内で扱う変数は、すべてアンダースコアつきのものに修正しておきます。

```kotlin
    private val _score = MutableLiveData<Int>()
    val score: LiveData<Int>
        get() = _score

    private val _displayScore = MutableLiveData<String>()
    val displayScore:LiveData<String>
        get() = _displayScore
```

## scoreの監視
仮に _**SecondFragment**_ 上の**score**が10に達したら強制的に _**FinalFragment**_ に遷移する、という要件があったとします。
これを実現するためには _**SecondViewModel**_ を下記の通り改修します。
なお`onToFinalComplete()`は画面再描画がされた場合に正常にBoolの値を保持するために必要な処理です。

```kotlin
class SecondViewModel : ViewModel() {

    private val _score = MutableLiveData<Int>()
    val score: LiveData<Int>
        get() = _score

    private val _displayScore = MutableLiveData<String>()
    val displayScore:LiveData<String>
        get() = _displayScore

    // 追記
    private val _eventToFinal = MutableLiveData<Boolean>()
    val eventToFinal:LiveData<Boolean>
        get() = _eventToFinal

    init {
        _score.value = 0
        _displayScore.value = ""
    }

    fun addScore() {
        _score.value = (score.value)?.plus(1)
        _displayScore.value = score.value.toString()

        // 修了判定
        if (_score.value == 10) {
            onToFinal()
        }
    }

    fun subtractScore() {
        _score.value = (score.value)?.minus(1)
        _displayScore.value = score.value.toString()
    }

    // 追記
    fun onToFinal() {
        _eventToFinal.value = true
    }

    // 追記
    fun onToFinalComplete() {
        _eventToFinal.value = false
    }
}
```

_**SecondFragment**_ は画面遷移をメソッド化しつつ、**eventToFinal**の**Observer**を追加しました。

```kotlin
class SecondFragment : Fragment() {

    private var _score = 1

    private lateinit var binding: FragmentSecondBinding
    private lateinit var viewModel: SecondViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_second,
            container,
            false
        )

        // viewModelの生成
        viewModel = ViewModelProvider(this).get(SecondViewModel::class.java)

        // Observerの登録
        viewModel.displayScore.observe(viewLifecycleOwner, Observer { newDisplayScore ->
            binding.scoreText.text = newDisplayScore
        })
        // 修了判定
        viewModel.eventToFinal.observe(viewLifecycleOwner, Observer { toFinal ->
            if (toFinal) {
                toFinalFragment()
            }
        })

        // クリックリスナを設定
        binding.toFinalFragmentButton.setOnClickListener { view: View ->
            toFinalFragment()
        }
        binding.addButton.setOnClickListener {
            viewModel.addScore()
            _score = viewModel.score.value?:0
        }
        binding.subtractButton.setOnClickListener {
            viewModel.subtractScore()
            _score = viewModel.score.value?:0
        }

        return binding.root
    }

    private fun toFinalFragment() {
        val action = SecondFragmentDirections.actionSecondFragmentToFinalFragment(_score)
        NavHostFragment.findNavController(this).navigate(action)
        viewModel.onToFinalComplete()
    }
}
```

## DataBindingの導入
これまでは _**Fragment.kt**_ クラスを経由して`View`と`ViewModel`を結び付けていましたが、両者を直接`DataBinding`で接合します。
これでイベントハンドラも不要となります。

## Data Bindingの設定
レイアウトファイルを開き、親ビューの直下に`<data>`ブロック、`<variable>`ブロックを作成します。
`Binding`として対応する _**FragmentのViewModel**_ を設定します。

```xml
<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="secondViewModel"
            type="com.warpstudio.android.viewmodelsample.SecondFragment" />
    </data>
```
なお今回は`Navigation`の利用を前提としているためレイアウトファイルは`DataBindin`g用のものに更新済みです。
これが未処理の場合は前回の記事を参照し、レイアウトを変更してください（`gradle`の更新も必要です）。

続いてレイアウトファイルの**AddButton**、**SubtractButton**、**toFinalFragmentButton**のリスナを`ViewModel`のメソッドに直接指定します。

```xml
        <Button
            android:id="@+id/addButton"
            // (略)
            android:onClick="@{() -> secondViewModel.addScore()"/>

        <Button
            android:id="@+id/subtractButton"
            // (略)
            android:onClick="@{() -> secondViewModel.subtractScore()}"/>
```

これにより、 _**SecondFragment**_ のイベントハンドラが不要となります。

画面部品の値を`Binding`先の`ViewModel`から直接設定するよう変更します。*score*を表示する`TextView`が該当します。

```xml
        <TextView
            android:id="@+id/scoreText"
            // (略)
            android:text="@{secondViewModel.displayScore}"
```

あわせて _**SecondFragment**_ で`binding.lifecycleOwner = viewLifecycleOwner`という記述を追加してください。当該画面部品に値を設定する`Observer`さえ不要となります。

最終的な _**SecondFragment**_ のコードは以下の通りとなります。

```kotlin
class SecondFragment : Fragment() {

    private var _score = 1

    private lateinit var binding: FragmentSecondBinding
    private lateinit var viewModel: SecondViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_second,
            container,
            false
        )

        // viewModelの生成
        viewModel = ViewModelProvider(this).get(SecondViewModel::class.java)

        // viewModelをbinding
        binding.secondViewModel = viewModel
        // LiveDataのためにviewLifecycleを設定
        binding.lifecycleOwner = viewLifecycleOwner

        // Observerの登録
        viewModel.displayScore.observe(viewLifecycleOwner, Observer { newDisplayScore ->
            binding.scoreText.text = newDisplayScore
        })
        viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
            _score = newScore
        })
        // 修了判定
        viewModel.eventToFinal.observe(viewLifecycleOwner, Observer { toFinal ->
            if (toFinal) {
                toFinalFragment()
            }
        })

        return binding.root
    }

    private fun toFinalFragment() {
        val action = SecondFragmentDirections.actionSecondFragmentToFinalFragment(_score)
        NavHostFragment.findNavController(this).navigate(action)
        viewModel.onToFinalComplete()
    }
}
```

# Recap
以上っ！
`ViewModel`の辺りからよく分からなくなってきた、なんて話をたまに周りから聞きます。
この記事では前回同様やはり概念的な内容は触れていないのですが、この辺はまぁ筋トレ的にあれこれ実装をして感覚をつかんでいくのも大事だと思うので、良かったらその際の参考にしてみてください。

次回は`Room`についてです。
ではでは。