【Android】2020年からの MVVM【実践】

![Android Studio 4.1.1](https://img.shields.io/badge/Android_Studio-4.1.1-brightgreen) ![androidx.lifecycle 2.2.0](https://img.shields.io/badge/androidx.lifecycle-2.2.0-brightgreen) ![Kotlin 1.4.20](https://img.shields.io/badge/Kotlin-1.4.20-blue) ![kotlinx.coroutines](https://img.shields.io/badge/kotlinx.coroutines-1.4.2-blue)

2020-10-27、[kotlinx.coroutines 1.4.0](https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.4.0) がリリースされました。
これにより [`SharedFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/index.html) と [`StateFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/index.html) が stable になり、
MVVM[^MVVM] が実装しやすくなりました。

[^MVVM]: ソフトウェアアーキテクチャの１つ。有名な MVC から派生したもの。

一方で、MVVM を十分に理解せずに使っている方がまだ多いように感じています。[^MVVM を十分に理解せず]

[^MVVM を十分に理解せず]: もしかしたら私もそうかもしれません。誤りなどあればご指摘いただければ幸いです。

そこで実際の実装の流れにそってサンプルプログラムの作りを見ながら、
MVVM はどのように設計すべきか、
これからはどのように実装するのがよいかを解説します。

なお、文中で紹介するソースコードは大幅に省略しています。
全体はこちらをご覧ください。
→ https://github.com/sdkei/Android_MVVM_Mediator_Login


<!---->


## 対象読者

次のことについて初級程度の知識を持っている方を対象にしています。

- Android Architecture Component
- Data Binding
- Kotlin Coroutines


<!---->


## コラム：Android 公式推奨アーキテクチャは MVVM ではない！

![Android 公式推奨アーキテクチャ](https://developer.android.com/topic/libraries/architecture/images/final-architecture.png?hl=ja)
皆さんご存じであろうこの図。
Android 公式の「[アプリ アーキテクチャ ガイド](https://developer.android.com/jetpack/guide?hl=ja)」 で「アプリの推奨アーキテクチャ」として紹介されているものです。

このガイドをもって「Google が推奨するアーキテクチャは MVVM だ」と言う方が非常に多いのですが、
**これは MVVM ではありません**。
理由は次のとおりです。

- そもそもこのガイドのどこにも MVVM だとは書かれていない。
- Model とよばれる領域が、Repository が参照している一部だけである。
- 次のような記述があり、ビジネスロジックを ViewModel が持つ設計になっている。

> ViewModel オブジェクトは、特定の UI コンポーネント（フラグメントやアクティビティなど）のデータを提供します。また、モデルとやり取りするデータ処理のビジネス ロジックを含んでいます。

小規模なアプリであればこのアーキテクチャの方がシンプルでよいかもしれません。
しかし大規模になると、Activity / Fragment に表示のロジックを持たせてしまって見通しが悪くなったり、ViewModel にビジネスロジックを持たせていることでそこが肥大化してしまったりします。
大規模なアプリでは MVVM を用いる方がよいでしょう。


<!---->


## サンプルプログラムとして作るもの

[結城浩著「増補改訂版 Java言語で学ぶ デザインパターン入門」](https://www.hyuki.com/dp/) 第16章「Mediator ― 相手は相談役1人だけ」のサンプルプログラムである「名前とパスワードを入力するログインダイアログ」を作ります。

<img alt="ログイン画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61280/60642a6e-2867-e10f-6e6f-95499c1881dd.png" width="50%">

使い方は次のとおりです。

- ゲストでのログイン［Guest］か登録ユーザーでのログイン［Login］かを選択する。
- 登録ユーザーでのログインの場合はユーザー ID［Username］とパスワード［Password］を入力する。
- ［OK］ボタンを押してログインする。
- キャンセルするなら［Cancel］ボタンを押す。

UI は次のように動作します。

- ［Guest］が選択された状態では  
  ［Username］と［Password］は入力不可。
- ［Login］が選択された状態では  
  ［Username］は入力可。
- ［Login］が選択された状態で  
  ［Username］に１文字以上入力されていれば  
  ［Password］は入力可。
- ［OK］ボタンは
    - ［Guest］が選択された状態では常に押下可。
    - ［Login］が選択された状態では  
      ［Username］と［Password］が共に１文字以上入力されていれば押下可。
- ［Cancel］ボタンは常に押下可。

ボタンが押されたときの動作は次のとおりです。

- ［OK］ボタンが押されたら
    - ［Guest］が選択されていれば次の画面に進む。
    - ［Login］が選択されていれば
        - ユーザー ID とパスワードが合っていれば次の画面に進む。
        - そうでなければエラーメッセージを表示する。
- ［Cancel］ボタンが押されたら前の画面に戻る。


<!---->


## 実装

### MVVM の３層

MVVM ではプログラムを次の３層に分けます。

- <font color="red">Model</font>
- <font color="blue">View</font> 
- <font color="green">ViewModel</font>

MVVM という名前はこれら３層の名前から来ています。

#### ３層の依存関係

これらは次のような依存関係になっています。
（MVVM という名前の順番とは異なることに注意！）

![３層の依存関係](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61280/a089ad30-6038-3740-3a34-77f976dd888d.png)

<!--
```plantuml
[View] -> [ViewModel] : 依存 >
[ViewModel] -> [Model] : 依存 >
```
-->

これは
<font color="red">Model</font> 層は <font color="green">ViewModel</font> 層や <font color="blue">View</font> 層のことを知らず、
<font color="green">ViewModel</font> 層は <font color="blue">View</font> 層のことを知らない、
ということです。

このため <font color="red">Model</font> 層 → <font color="green">ViewModel</font> 層 → <font color="blue">View</font> 層の順で実装すると一層ずつ完結できて[^一層ずつ完結できる]やりやすいです。

[^一層ずつ完結できる]: 実際の開発ではそう都合よくは行きませんが。

今回もこの順番で進めていきます。

#### パッケージ構成

パッケージはトップレベルで３つに分けるのがおすすめです。
そうすれば各コードがどの層に属するものか分かりやすく、
誤った依存を機械的に発見できます。

今回の実装だと次のようになります。

```plaintext:パッケージ
io.github.sdkei.loginmvvm.view
io.github.sdkei.loginmvvm.viewmodel
io.github.sdkei.loginmvvm.model
```

大規模なアプリであれば層ごとにモジュールを分けてしまうのもよいかもしれません。


<!---->


### <font color="red">Model</font> 層


#### <font color="red">Model</font> 層の役割

<font color="red">Model</font> 層はビジネスロジックとそれに関するデータ管理を受け持ちます。

UI 仕様には依存しないため、
コマンドライン版や別のプラットフォームでも（言語やバイナリの互換性があれば）使い回せます。

今回のビジネスロジックはなんでしょうか？

——「ログイン可否判断」ですね。

#### ユーザー管理クラス

ログイン可否判断をするには、
登録済みのユーザーとそのパスワードが管理されている必要があります。

その管理を行うクラス `UserRepository` を作成します。
Repository パターンですね。

```Kotlin:UserRepository.kt
package io.github.sdkei.loginmvvm.model

// 省略

/** 登録ユーザーを管理するリポジトリー。 */
object UserRepository {

    // 省略

    /**
     * 登録ユーザーを認証する。
     *
     * @return 成功したかどうか。
     */
    suspend fun authenticate(userId: String, password: String): Boolean {
        // 省略
    }
}
```

実際にはユーザーを登録する機能などもありますが、
当記事で説明する範囲ではユーザー認証機能だけを使います。

認証する際には、通常、DB やネットワークなどへの I/O が発生します。
そこで認証関数は `suspend` にしました。
（GitHub に上げた実装では~~手抜きで~~簡単のために `Map` を使っています。）


#### ログイン可否判断クラス

ログイン可否判断を行うクラス `LoginUseCase` を作成します。

このクラスではログイン中のユーザーの管理も行います。

```Kotlin:LoginUseCase.kt
package io.github.sdkei.loginmvvm.model

// 省略

/** ログインの管理を行う。 */
@Suppress("ObjectPropertyName")
object LoginUseCase {

    /** ゲストユーザーのユーザー ID。 */
    const val GUEST_USER_ID = ""

    private val mutex = Mutex()

    private val userRepository: UserRepository
        get() = UserRepository

    /** ログイン中のユーザーのユーザー ID。 */
    val loginUserId: StateFlow<String?>
        get() = _loginUserId
    private val _loginUserId = MutableStateFlow<String?>(null)

    /**
     * 登録ユーザーでログインする。
     *
     * [mutex] でロックした状態で呼び出さ **ない** こと。
     *
     * @return 成功したかどうか。
     */
    suspend fun loginRegisteredUser(userId: String, password: String): Boolean {
        mutex.withLock {
            checkNotLogin()

            return userRepository.authenticate(userId, password).also { isSucceeded ->
                if (isSucceeded) {
                    _loginUserId.value = userId
                }
            }
        }
    }

    /**
     * ゲストユーザーでログインする。
     *
     * [mutex] でロックした状態で呼び出さ **ない** こと。
     */
    suspend fun loginGuest() {
        mutex.withLock {
            checkNotLogin()

            _loginUserId.value = GUEST_USER_ID
        }
    }

    /**
     * ログアウトする。
     *
     * [mutex] でロックした状態で呼び出さ **ない** こと。
     */
    suspend fun logout() {
        mutex.withLock {
            checkLogin()

            _loginUserId.value = null
        }
    }

    /**
     * ログイン中であることを確認する。
     *
     * [mutex] でロックした状態で呼び出すこと。
     */
    private fun checkLogin() {
        check(mutex.isLocked)

        check(loginUserId.value != null) { "未だログインされていません。" }
    }

    /**
     * ログイン中でないことを確認する。
     *
     * [mutex] でロックした状態で呼び出すこと。
     */
    private fun checkNotLogin() {
        check(mutex.isLocked)

        check(loginUserId.value == null) { "既にログインされています。" }
    }
}
```

<!--
##### <font color="red">Model</font> 層のオブジェクトの状態を表すプロパティには `StateFlow` が適している
-->

ログイン中のユーザーを [`MutableStateFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-state-flow/) 型の `private` プロパティ `_loginUserId` で管理し、
`StateFlow` 型の `public` プロパティ `loginUserId` として <font color="green">ViewModel</font> 層に公開しています。
`StateFlow` を使うことで状態の保持ができ、またそれを監視しているオブジェクトに対して状態変更の通知を行えます。

[`LiveData`](https://developer.android.com/reference/androidx/lifecycle/LiveData) とよく似ていますね。
しかし <font color="red">Model</font> 層で使うのには `LiveData` より `StateFlow` の方が適しています。

`LiveData` では、

- 監視するために使われる [`observe`](https://developer.android.com/reference/androidx/lifecycle/LiveData#observe(androidx.lifecycle.LifecycleOwner,%20androidx.lifecycle.Observer<?%20super%20T>)) 関数は引数に [`LifecycleOwner`](https://developer.android.com/reference/androidx/lifecycle/LifecycleOwner) オブジェクトを渡す必要があります。  
  しかし <font color="red">Model</font> 層や <font color="green">ViewModel</font> 層には `LifecycleOwner` がありません。
- `LifecycleOwner` が不要な [`observeForever`](https://developer.android.com/reference/androidx/lifecycle/LiveData#observeForever(androidx.lifecycle.Observer<?%20super%20T>)) 関数もありますが、
これは監視が不要になったときに明示的に `removeObserver` 関数を呼び出さなければメモリリークが起こります。

`StateFlow` であれば、

- [`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) を使い、それがアクティブである間だけ監視を行うことができます。  
  `CoroutineScope` は、<font color="green">ViewModel</font> 層であれば `ViewModel` クラスの拡張プロパティ [`viewModelScope`](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#viewmodelscope) で取得できますし、<font color="red">Model</font> 層でも簡単に作ることができます。

<!--

##### コルーチンの排他制御には `Mutex` を使う

コルーチンを使っているので排他制御に Java の排他制御機構は使えません。
代わりに [`Mutex`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/) を用いています。

注意！
Mutex は再入可能（reentrant）では **ない** ので
[`withLock`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/with-lock.html) のブロック内などでのロック中に再帰的にさらにロックをすると
デッドロックしてしまうことに注意してください。

-->


<!---->


### <font color="green">ViewModel</font> 層

#### <font color="green">ViewModel</font> 層の役割

<font color="green">ViewModel</font> 層は次のことを受け持ちます。

- UI の状態の保持（ラジオボタンはどれが選択されているか、テキストフィールドには何が入力されているか、など）
- UI コンポーネントに直接依存しない表示のロジック（テキストフィールドが空でなくなったらボタンを押下可能にする、など）

UI コンポーネント同士の連携は <font color="blue">View</font> 層で行ってはいけません。それは <font color="green">ViewModel</font> 層の仕事です。
<font color="green">ViewModel</font> 層は UI コンポーネント（`View` クラスなど）の型などを知っていてはいけません。それは <font color="blue">View</font> 層の仕事です。

例えば「テキストフィールドが空でなくなったらボタンを押下可能にする」のであれば、
`EditText` オブジェクトや `Button` オブジェクトを直接参照するのではなく、
次のようにします。

- テキストフィールドの内容を保持するプロパティとボタンが押下可能かどうかを表すプロパティを <font color="green">ViewModel</font> 層に用意し、<font color="blue">View</font> 層に公開する。
- <font color="green">ViewModel</font> 層では、テキストフィールドの内容を保持するプロパティを監視し、値が変更されたらボタンが押下可能かどうかを表すプロパティの値を変更する。
- <font color="blue">View</font> 層では、それらのプロパティの値を監視して UI コンポーネントの表示に反映し、ユーザーによって UI コンポーネントが操作されたらそれをプロパティの値に反映させる。

これを実現するためには、<font color="green">ViewModel</font> 層は UI の外部仕様は知っている必要があります。

<font color="green">ViewModel</font> 層から <font color="blue">View</font> 層へは、基本的に UI のカタマリ１つごとに１つのオブジェクトを公開します。
Android では UI のカタマリは `Activity` と `Fragment` になるはずです。
そしてそれに対して <font color="green">ViewModel</font> 層から公開するオブジェクトは、ライフサイクル管理の都合上、 `ViewModel` クラスになるはずです。
結果として `Activity` オブジェクトおよび `Fragment` オブジェクトと `ViewModel` オブジェクトは一対一対応になります。

なお、「<font color="green">ViewModel</font> 層には `ViewModel` クラスしか置いてはいけない」ということは**ありません**。
その実装の中で `ViewModel` クラスでないオブジェクトを使ってよいですし、
それらを <font color="blue">View</font> 層 に公開しても問題ありません。

ではここから、<font color="green">ViewModel</font> 層から <font color="blue">View</font> 層に公開するクラスである `LoginViewModel`を実装していきましょう。


#### 「状態」を <font color="blue">View</font> 層に公開する

詳しくは <font color="blue">View</font> 層の説明で述べますが、
<font color="blue">View</font> 層では「コードビハインド」をできるだけ減らすべきです。
そのためにはレイアウトファイルでデータバインディングできる形で
<font color="green">ViewModel</font> 層が持つ「状態」を <font color="blue">View</font> 層に公開する必要があります。
双方向データバインディングする必要があるものは `MutableLiveData` 型、
単方向でよいものは `LiveData` 型で公開します。

```Kotlin:LoginViewModel.kt
package io.github.sdkei.loginmvvm.viewmodel

// 省略

/** ログイン画面の [ViewModel]。 */
class LoginViewModel : ViewModel() {

    /** ゲスト／登録済みユーザーのどちらが選択されているか。 */
    val userType = MutableLiveData<UserType>(UserType.GUEST)

    /** ユーザー ID 入力欄の値。 */
    val userId = MutableLiveData<String>("")

    /** ユーザー ID 入力欄を有効にするかどうか。 */
    val isUserIdEnabled: LiveData<Boolean>
        get() = _isUserIdEnabled
    private val _isUserIdEnabled = MutableLiveData(false)

    /** パスワード入力欄の値。 */
    val password = MutableLiveData<String>("")

    /** パスワード入力欄を有効にするかどうか。 */
    val isPasswordEnabled: LiveData<Boolean>
        get() = _isPasswordEnabled
    private val _isPasswordEnabled = MutableLiveData(false)

    /** OK ボタンを有効にするかどうか。 */
    val isOkEnabled: LiveData<Boolean>
        get() = _okEnabled
    private val _okEnabled = MutableLiveData(true)

    /** キャンセルボタンを有効にするかどうか。 */
    val isCancelEnabled: LiveData<Boolean> = MutableLiveData(true) // 常に有効にする。
}
```

ゲスト／登録済みユーザーのどちらが選択されているかを表すプロパティ `userType` は、
`UserType` 型の値（を持つ `MutableLiveData`）です。
この型は enum クラスであり、<font color="red">Model</font> 層で定義してあります。

#### UI の状態を表すプロパティ同士の連携

次に、いずれかの UI コンポーネントの状態が変わったら他の UI コンポーネントの状態を変える、という処理を実装します。
そのためには上で実装した `LiveData` 型プロパティの値の変更を監視する必要があります。

今回は複数のオブジェクトを一カ所で監視して、いずれかが変更されたら複数のオブジェクトを変更するようにします。

複数の `LiveData` オブジェクトを監視して、いずれかの値が変更されたときに**ある１つの `LiveData` オブジェクト**の値を変更する場合であれば、その「ある１つの `LiveData` オブジェクト」を [`MediatorLiveData`](https://developer.android.com/reference/kotlin/androidx/lifecycle/MediatorLiveData) にするとよいでしょう。
しかしこれは複数のオブジェクトの値を変更する場合には適していません。
次の理由からです。

- 自身が `LiveData` なので、自身以外の値を変更するとわかりにくくなる。
- 自身が監視されていないと動作しない（他のオブジェクトを監視しない）。

そこで [`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) に変換して、[`ViewModel.viewModelScope`](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#viewmodelscope) のスコープで監視するようにします。

```Kotlin:LoginViewModel.kt
package io.github.sdkei.loginmvvm.viewmodel

// 省略

/** ログイン画面の [ViewModel]。 */
class LoginViewModel : ViewModel() {

    // 省略

    // プロパティが更新されたときに、それに依存する他のプロパティが更新されるように監視を開始する。
    init {
        listOf(userType, userId, password).forEach { liveData ->
            liveData.asFlow()
                .onEach { updateStatus() }
                .launchIn(viewModelScope)
        }
    }

    /** 各プロパティの値を、それらが依存する他のプロパティの現在の値に合わせて更新する。 */
    // 「増補改訂版 Java 言語で学ぶデザインパターン入門（結城浩著）」の「16章 Mediator」での
    // サンプルプログラムにある colleagueChanged メソッドに相当する。
    private fun updateStatus() {
        when (userType.value) {
            UserType.GUEST -> {
                _isUserIdEnabled.value = false
                _isPasswordEnabled.value = false
                _okEnabled.value = true
            }
            UserType.REGISTERED -> {
                _isUserIdEnabled.value = true
                if (userId.value.isNullOrEmpty()) {
                    _isPasswordEnabled.value = false
                    _okEnabled.value = false
                } else {
                    _isPasswordEnabled.value = true
                    _okEnabled.value = password.value.isNullOrEmpty().not()
                }
            }
            null -> Unit
        }.exhaustive
    }
}
```

ちなみに、`when` のブロックの後ろについている `exhaustive` は、
`when` ですべての分岐が網羅されていなければコンパイルエラーとするためのユーティリティ拡張プロパティです。
→[Kotlin の when で enum を評価するときに、分岐ケースの列挙が不完全であればビルドエラーにする workaround](https://qiita.com/tatsuhama/items/f9cbec8aa1f8b72a202f)

#### コマンドとメッセンジャー

最後に、ログインとキャンセルの処理を実装します。

これらは MVVM では本来「コマンド」[^コマンド]として <font color="blue">View</font> 層から呼び出せるように公開するべきものです。
Android には「コマンド」の仕組みがないため、単なる関数として公開します。

[^コマンド]: ここでは詳細は述べません。興味があれば次の記事をご覧ください。https://www.atmarkit.co.jp/fdotnet/chushin/greatblogentry_02/greatblogentry_02_03.html

これらの関数を実行した結果は返値ではなく「メッセンジャーパターン」を使って発信します。
コマンドに相当する関数はレイアウトファイルでボタン押下などに割り当てられるため、返値を返しても <font color="blue">View</font> 層では処理できないからです。
また、ログインとキャンセルの結果である「ログインに成功した」「ログインに失敗した」「キャンセルされた」は「状態」ではなくある瞬間の出来事なので、状態を表すオブジェクトである `LiveData` を使うのは適切ではありません。

メッセンジャーパターンは、「メッセンジャー」オブジェクトを介してメッセージ（情報）を送受信するデザインパターンです。
送信側はメッセンジャーにメッセージを渡し、受信側はメッセンジャーを監視することでメッセージを受け取ります。
Observer パターンの一種ですね。

![メッセンジャーパターンのオブジェクト図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61280/5fcaae9a-b9cc-66b4-c3dd-4c60a9f135c6.png)

<!--
```plantuml
object View
object Messenger
object ViewModel

View -> Messenger : 監視 >
Messenger <- ViewModel : < 送信
```
-->

メッセンジャーオブジェクトには [`SharedFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/index.html) クラスが適しています。
監視を開始する際に渡した `CoroutineScope` がアクティブでなくなれば自動的に監視が解除されるので、監視の解除忘れによるメモリリークが起こらないからです。

メッセージオブジェクトには sealed クラスを使うのがよいでしょう。
サブクラスごとに全く異なる情報を持たせられ、
すべてのサブクラスが既知なので受信側でキャストして
サブクラスごとに異なる処理に回すことができます。
（このあたりについては受信側である <font color="blue">View</font> 層を実装する際に見ていきます。）

```Kotlin:LoginViewModel.kt
package io.github.sdkei.loginmvvm.viewmodel

// 省略

/** ログイン画面の [ViewModel]。 */
class LoginViewModel : ViewModel() {

    /** メッセージを送出する [Flow]。 */
    val message: SharedFlow<Message>
        get() = _message
    private val _message = MutableSharedFlow<Message>()

    // 省略

    private val loginUseCase = LoginUseCase

    // 省略

    /** ログインし、ログイン後の画面に遷移する。 */
    fun login() {
        when (userType.value) {
            UserType.GUEST -> {
                viewModelScope.launch(Dispatchers.Main) {
                    loginUseCase.loginGuest()

                    Message.Succeeded(LoginUseCase.GUEST_USER_ID).also {
                        _message.emit(it)
                    }
                }
            }
            UserType.REGISTERED -> {
                val userId = checkNotNull(userId.value) { "ユーザーID が null です。" }
                    .also { check(it.isNotEmpty()) { "ユーザーID が空です。" } }
                val password = checkNotNull(password.value) { "パスワードが null です。" }
                    .also { check(it.isNotEmpty()) { "パスワードが空です。" } }

                viewModelScope.launch(Dispatchers.Main) {
                    val isSucceeded = loginUseCase.loginRegisteredUser(userId, password)
                    if (isSucceeded.not()) {
                        _message.emit(Message.Failed(userId))
                        return@launch
                    }

                    Message.Succeeded(userId).also {
                        _message.emit(it)
                    }
                }
            }
            null -> throw IllegalStateException("ユーザー種別が選択されていません。")
        }.exhaustive
    }

    /** ログインをキャンセルし、ログイン前の画面に遷移する。 */
    fun cancel() {
        viewModelScope.launch(Dispatchers.Main) {
            _message.emit(Message.Cancelled)
        }
    }

    ////////////////////////////////////////////////////////////////
    // ネストされた型
    //

    /** [message] プロパティから送出されるメッセージ。 */
    sealed class Message {
        /** キャンセルされたためログイン前の画面に遷移することを要求するメッセージ。 */
        object Cancelled : Message()

        /** ログインに成功したのでログイン後の画面に遷移することを要求するメッセージ。 */
        data class Succeeded(
            val userId: String
        ) : Message()

        /** ログインに失敗したのでそのことをユーザーに通知することを要求するメッセージ。 */
        data class Failed(
            val userId: String
        ) : Message()
    }
}
```


<!----->


### <font color="blue">View</font> 層

#### <font color="blue">View</font> 層の役割

<font color="blue">View</font> 層は UI コンポーネントに依存した処理を受け持ちます。

<font color="green">ViewModel</font> 層の説明でも述べましたが、
<font color="blue">View</font> 層では UI コンポーネント同士の連携を実装してはいけません。
それは <font color="green">ViewModel</font> 層の仕事です。

Android では `Activity` と `Fragment` は UI コンポーネント（`View` クラス）に依存しますので <font color="blue">View</font> 層になります。

#### コードビハインド

<font color="blue">View</font> 層からはできる限り「コードビハインド」を減らすべきです。
コードビハインドというのは手続き型の言語（Kotlin や Java など）によるコードのことです。
宣言型の言語（XML など）によるコードはコードビハインドではありません。
宣言的に実装することでコードの見通しがよくなります。

レイアウトファイルでレイアウトを実装し、
レイアウトファイル上で UI コンポーネントの属性と `ViewModel` オブジェクトのプロパティとのデータバインディングを行うことで、
コードビハインドを大幅に減らすことができます。

#### レイアウトファイル

ではレイアウトファイルを作成しましょう。

```XML:login_fragment.xml
<!-- 本質でない属性は省略 -->
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <variable
            name="viewModel"
            type="io.github.sdkei.loginmvvm.viewmodel.LoginViewModel" />

        <!-- 省略 -->

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        tools:context=".view.LoginFragment">

        <RadioGroup
            android:id="@+id/radio_group_user_type"
            android:checkedButton="@={UserTypeToRadioButtonIdResConverter.convert(viewModel.userType)}">

            <RadioButton
                android:id="@+id/radio_button_guest_user" />

            <RadioButton
                android:id="@+id/radio_button_registered_user" />

        </RadioGroup>

        <TextView
            android:id="@+id/text_view_user_id" />

        <EditText
            android:id="@+id/edit_text_user_id"
            android:enabled="@{viewModel.isUserIdEnabled}"
            android:text="@={viewModel.userId}" />

        <TextView
            android:id="@+id/text_view_password" />

        <EditText
            android:id="@+id/edit_text_password"
            android:enabled="@{viewModel.isPasswordEnabled}"
            android:text="@={viewModel.password}" />

        <Button
            android:id="@+id/button_ok"
            android:enabled="@{viewModel.isOkEnabled}"
            android:onClick="@{() -> viewModel.login()}" />

        <Button
            android:id="@+id/button_cancel"
            android:enabled="@{viewModel.isCancelEnabled}"
            android:onClick="@{() -> viewModel.cancel()}" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

ラジオボタンの選択状態とユーザー ID、パスワードは、ユーザーによって変更されたときに <font color="blue">View</font> 層から <font color="green">ViewModel</font> 層にも伝える必要があるため、双方向データバインディングします。
各 UI コンポーネントの有効／無効の指定は <font color="green">ViewModel</font> 層から <font color="blue">View</font> 層への一方向でよいため単方向データバインディングです。

ボタンがクリックされたときの処理では `viewModel` オブジェクトが持つ関数を呼び出しています。

#### コンバーター

どのラジオボタンが選択されているかは `RadioButtonGroup` 要素の `android:checkedButton` 属性で指定しますが、
その値は `RadioButton` オブジェクトのリソース ID です。
これは <font color="blue">View</font> 層にあるレイアウトファイルで定義された値であるため、
<font color="green">ViewModel</font> 層で使ってはいけません。

そこで <font color="green">ViewModel</font> 層からは公開するプロパティは `UserType` 型（の値を持つ `MutableLiveData`）にして、
それを <font color="blue">View</font> 層でコンバーターを使って `@IdRes Int` 型に変換しています。

```XML:login_fragment.xml
        <!-- 省略 -->

        <!-- 本質でない属性は省略 -->
        <RadioGroup
            android:id="@+id/radio_group_user_type"
            android:checkedButton="@={UserTypeToRadioButtonIdResConverter.convert(viewModel.userType)}">

        <!-- 省略 -->
```

```Kotlin:UserTypeToRadioButtonIdResConverter.kt
package io.github.sdkei.loginmvvm.view.converter

// 省略

/** [UserType] とリソース ID を相互変換するコンバーター。 */
object UserTypeToRadioButtonIdResConverter {
    @JvmStatic
    @IdRes
    fun convert(userType: UserType?): Int =
        when (userType) {
            UserType.GUEST -> R.id.radio_button_guest_user
            UserType.REGISTERED -> R.id.radio_button_registered_user
            null -> 0
        }.exhaustive

    @InverseMethod("convert")
    @JvmStatic
    fun inverseConvert(@IdRes idRes: Int): UserType? =
        when (idRes) {
            R.id.radio_button_guest_user -> UserType.GUEST
            R.id.radio_button_registered_user -> UserType.REGISTERED
            else -> null
        }.exhaustive
}
```

なお、コンバーターのコードはコードビハインドとは考えません。
うまく使っていきましょう。

`view` パッケージ直下のファイルが多い場合は、コンバーター用のサブパッケージを切ると見通しがよくなります。

#### `Fragment`

最後に `Fragment` クラスを作成します。

まず、バインディングの設定を行います。

```Kotlin:LoginFragment.kt
package io.github.sdkei.loginmvvm.view

// 省略

/** ログイン画面。 */
class LoginFragment : Fragment() {
    private val viewModel by viewModels<LoginViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View =
        LoginFragmentBinding.inflate(inflater, container, false).also {
            it.viewModel = viewModel
            it.lifecycleOwner = viewLifecycleOwner
        }.root
}
```

そしてメッセージの受信開始と、受信したときの処理を実装します。

```Kotlin:LoginFragment.kt
package io.github.sdkei.loginmvvm.view

// 省略

/** ログイン画面。 */
class LoginFragment : Fragment() {
    private val viewModel by viewModels<LoginViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View =
        LoginFragmentBinding.inflate(inflater, container, false).also {
            it.viewModel = viewModel
            it.lifecycleOwner = viewLifecycleOwner
        }.also {
            // メッセージの監視を開始する。
            // このタイミングである必要はないが、
            // データバインディングとタイミングを合わせておく。
            viewModel.message
                .onEach { onMessage(it) }
                .launchIn(lifecycleScope)
            // ↑lifecycleScope は Dispatchers.Main に束縛されているため、
            // ↑onMessage 関数はメインスレッドで呼び出される。
        }.root

    /** [ViewModel] から送られたメッセージを処理する。 */
    private fun onMessage(message: Message) {
        when (message) {
            is Message.Cancelled -> onMessageCancelled(message)
            is Message.Succeeded -> onMessageSucceeded(message)
            is Message.Failed -> onMessageFailed(message)
        }.exhaustive
    }

    @Suppress("UNUSED_PARAMETER")
    private fun onMessageCancelled(message: Message.Cancelled) {
        findNavController().navigate(R.id.action_loginFragment_to_beforeLoginFragment)
    }

    @Suppress("UNUSED_PARAMETER")
    private fun onMessageSucceeded(message: Message.Succeeded) {
        findNavController().navigate(R.id.action_loginFragment_to_afterLoginFragment)
    }

    @Suppress("UNUSED_PARAMETER")
    private fun onMessageFailed(message: Message.Failed) {
        // ログインできなかったことを通知するダイアログを表示する。
        AlertDialog.Builder(requireContext())
            .setMessage(R.string.dialog_message_login_failed)
            .setPositiveButton(R.string.button_close) { dialog, _ ->
                dialog.cancel()
            }
            .show()
    }
}
```

メッセージを受信したら、その具象型に応じた処理に回しています。
sealed クラスなのですべての具象型に対応できていることをコンパイル時に保証できます。
（ここでも拡張プロパティ `exhaustive` を使ってすべての分岐を網羅していることを保証しています。）

コードビハインドをなくすことはできませんが、
データバインディングとメッセージの処理だけの簡潔なコードになりました。


<!----->


## おわりに

長くなりましたが、MVVM を用いた実装の方法を紹介しました。
各層の役割と層同士の依存関係を守って、見通しのよいコードにしていきましょう。

誤りなどがありましたらご指摘いただければ幸いです。


<!---->


## 参考

- [MVVMパターンの常識 ― 「M」「V」「VM」の役割とは？ - ＠IT](https://www.atmarkit.co.jp/fdotnet/chushin/greatblogentry_02/greatblogentry_02_01.html)

／以上