2020-10-27、kotlinx.coroutines 1.4.0 がリリースされました。
これにより SharedFlow
と StateFlow
が stable になり、
MVVM1 が実装しやすくなりました。
一方で、MVVM を十分に理解せずに使っている方がまだ多いように感じています。^MVVM を十分に理解せず
そこで実際の実装の流れにそってサンプルプログラムの作りを見ながら、
MVVM はどのように設計すべきか、
これからはどのように実装するのがよいかを解説します。
なお、文中で紹介するソースコードは大幅に省略しています。
全体はこちらをご覧ください。
→ https://github.com/sdkei/Android_MVVM_Mediator_Login
対象読者
次のことについて初級程度の知識を持っている方を対象にしています。
- Android Architecture Component
- Data Binding
- Kotlin Coroutines
コラム:Android 公式推奨アーキテクチャは MVVM ではない!
皆さんご存じであろうこの図。
Android 公式の「アプリ アーキテクチャ ガイド」 で「アプリの推奨アーキテクチャ」として紹介されているものです。
このガイドをもって「Google が推奨するアーキテクチャは MVVM だ」と言う方が非常に多いのですが、
これは MVVM ではありません。
理由は次のとおりです。
- そもそもこのガイドのどこにも MVVM だとは書かれていない。
- Model とよばれる領域が、Repository が参照している一部だけである。
- 次のような記述があり、ビジネスロジックを ViewModel が持つ設計になっている。
ViewModel オブジェクトは、特定の UI コンポーネント(フラグメントやアクティビティなど)のデータを提供します。また、モデルとやり取りするデータ処理のビジネス ロジックを含んでいます。
小規模なアプリであればこのアーキテクチャの方がシンプルでよいかもしれません。
しかし大規模になると、Activity / Fragment に表示のロジックを持たせてしまって見通しが悪くなったり、ViewModel にビジネスロジックを持たせていることでそこが肥大化してしまったりします。
大規模なアプリでは MVVM を用いる方がよいでしょう。
サンプルプログラムとして作るもの
結城浩著「増補改訂版 Java言語で学ぶ デザインパターン入門」 第16章「Mediator ― 相手は相談役1人だけ」のサンプルプログラムである「名前とパスワードを入力するログインダイアログ」を作ります。
使い方は次のとおりです。
- ゲストでのログイン[Guest]か登録ユーザーでのログイン[Login]かを選択する。
- 登録ユーザーでのログインの場合はユーザー ID[Username]とパスワード[Password]を入力する。
- [OK]ボタンを押してログインする。
- キャンセルするなら[Cancel]ボタンを押す。
UI は次のように動作します。
- [Guest]が選択された状態では
[Username]と[Password]は入力不可。 - [Login]が選択された状態では
[Username]は入力可。 - [Login]が選択された状態で
[Username]に1文字以上入力されていれば
[Password]は入力可。 - [OK]ボタンは
- [Guest]が選択された状態では常に押下可。
- [Login]が選択された状態では
[Username]と[Password]が共に1文字以上入力されていれば押下可。
- [Cancel]ボタンは常に押下可。
ボタンが押されたときの動作は次のとおりです。
- [OK]ボタンが押されたら
- [Guest]が選択されていれば次の画面に進む。
- [Login]が選択されていれば
- ユーザー ID とパスワードが合っていれば次の画面に進む。
- そうでなければエラーメッセージを表示する。
- [Cancel]ボタンが押されたら前の画面に戻る。
実装
MVVM の3層
MVVM ではプログラムを次の3層に分けます。
- Model
- View
- ViewModel
MVVM という名前はこれら3層の名前から来ています。
3層の依存関係
これらは次のような依存関係になっています。
(MVVM という名前の順番とは異なることに注意!)
これは
Model 層は ViewModel 層や View 層のことを知らず、
ViewModel 層は View 層のことを知らない、
ということです。
このため Model 層 → ViewModel 層 → View 層の順で実装すると一層ずつ完結できて2やりやすいです。
今回もこの順番で進めていきます。
パッケージ構成
パッケージはトップレベルで3つに分けるのがおすすめです。
そうすれば各コードがどの層に属するものか分かりやすく、
誤った依存を機械的に発見できます。
今回の実装だと次のようになります。
io.github.sdkei.loginmvvm.view
io.github.sdkei.loginmvvm.viewmodel
io.github.sdkei.loginmvvm.model
大規模なアプリであれば層ごとにモジュールを分けてしまうのもよいかもしれません。
Model 層
Model 層の役割
Model 層はビジネスロジックとそれに関するデータ管理を受け持ちます。
UI 仕様には依存しないため、
コマンドライン版や別のプラットフォームでも(言語やバイナリの互換性があれば)使い回せます。
今回のビジネスロジックはなんでしょうか?
——「ログイン可否判断」ですね。
ユーザー管理クラス
ログイン可否判断をするには、
登録済みのユーザーとそのパスワードが管理されている必要があります。
その管理を行うクラス UserRepository
を作成します。
Repository パターンですね。
package io.github.sdkei.loginmvvm.model
// 省略
/** 登録ユーザーを管理するリポジトリー。 */
object UserRepository {
// 省略
/**
* 登録ユーザーを認証する。
*
* @return 成功したかどうか。
*/
suspend fun authenticate(userId: String, password: String): Boolean {
// 省略
}
}
実際にはユーザーを登録する機能などもありますが、
当記事で説明する範囲ではユーザー認証機能だけを使います。
認証する際には、通常、DB やネットワークなどへの I/O が発生します。
そこで認証関数は suspend
にしました。
(GitHub に上げた実装では手抜きで簡単のために Map
を使っています。)
ログイン可否判断クラス
ログイン可否判断を行うクラス LoginUseCase
を作成します。
このクラスではログイン中のユーザーの管理も行います。
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) { "既にログインされています。" }
}
}
ログイン中のユーザーを MutableStateFlow
型の private
プロパティ _loginUserId
で管理し、
StateFlow
型の public
プロパティ loginUserId
として ViewModel 層に公開しています。
StateFlow
を使うことで状態の保持ができ、またそれを監視しているオブジェクトに対して状態変更の通知を行えます。
LiveData
とよく似ていますね。
しかし Model 層で使うのには LiveData
より StateFlow
の方が適しています。
LiveData
では、
- 監視するために使われる
observe
関数は引数にLifecycleOwner
オブジェクトを渡す必要があります。
しかし Model 層や ViewModel 層にはLifecycleOwner
がありません。 -
LifecycleOwner
が不要なobserveForever
関数もありますが、
これは監視が不要になったときに明示的にremoveObserver
関数を呼び出さなければメモリリークが起こります。
StateFlow
であれば、
-
CoroutineScope
を使い、それがアクティブである間だけ監視を行うことができます。
CoroutineScope
は、ViewModel 層であればViewModel
クラスの拡張プロパティviewModelScope
で取得できますし、Model 層でも簡単に作ることができます。
ViewModel 層
ViewModel 層の役割
ViewModel 層は次のことを受け持ちます。
- UI の状態の保持(ラジオボタンはどれが選択されているか、テキストフィールドには何が入力されているか、など)
- UI コンポーネントに直接依存しない表示のロジック(テキストフィールドが空でなくなったらボタンを押下可能にする、など)
UI コンポーネント同士の連携は View 層で行ってはいけません。それは ViewModel 層の仕事です。
ViewModel 層は UI コンポーネント(View
クラスなど)の型などを知っていてはいけません。それは View 層の仕事です。
例えば「テキストフィールドが空でなくなったらボタンを押下可能にする」のであれば、
EditText
オブジェクトや Button
オブジェクトを直接参照するのではなく、
次のようにします。
- テキストフィールドの内容を保持するプロパティとボタンが押下可能かどうかを表すプロパティを ViewModel 層に用意し、View 層に公開する。
- ViewModel 層では、テキストフィールドの内容を保持するプロパティを監視し、値が変更されたらボタンが押下可能かどうかを表すプロパティの値を変更する。
- View 層では、それらのプロパティの値を監視して UI コンポーネントの表示に反映し、ユーザーによって UI コンポーネントが操作されたらそれをプロパティの値に反映させる。
これを実現するためには、ViewModel 層は UI の外部仕様は知っている必要があります。
ViewModel 層から View 層へは、基本的に UI のカタマリ1つごとに1つのオブジェクトを公開します。
Android では UI のカタマリは Activity
と Fragment
になるはずです。
そしてそれに対して ViewModel 層から公開するオブジェクトは、ライフサイクル管理の都合上、 ViewModel
クラスになるはずです。
結果として Activity
オブジェクトおよび Fragment
オブジェクトと ViewModel
オブジェクトは一対一対応になります。
なお、「ViewModel 層には ViewModel
クラスしか置いてはいけない」ということはありません。
その実装の中で ViewModel
クラスでないオブジェクトを使ってよいですし、
それらを View 層 に公開しても問題ありません。
ではここから、ViewModel 層から View 層に公開するクラスである LoginViewModel
を実装していきましょう。
「状態」を View 層に公開する
詳しくは View 層の説明で述べますが、
View 層では「コードビハインド」をできるだけ減らすべきです。
そのためにはレイアウトファイルでデータバインディングできる形で
ViewModel 層が持つ「状態」を View 層に公開する必要があります。
双方向データバインディングする必要があるものは MutableLiveData
型、
単方向でよいものは LiveData
型で公開します。
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 クラスであり、Model 層で定義してあります。
UI の状態を表すプロパティ同士の連携
次に、いずれかの UI コンポーネントの状態が変わったら他の UI コンポーネントの状態を変える、という処理を実装します。
そのためには上で実装した LiveData
型プロパティの値の変更を監視する必要があります。
今回は複数のオブジェクトを一カ所で監視して、いずれかが変更されたら複数のオブジェクトを変更するようにします。
複数の LiveData
オブジェクトを監視して、いずれかの値が変更されたときにある1つの LiveData
オブジェクトの値を変更する場合であれば、その「ある1つの LiveData
オブジェクト」を MediatorLiveData
にするとよいでしょう。
しかしこれは複数のオブジェクトの値を変更する場合には適していません。
次の理由からです。
- 自身が
LiveData
なので、自身以外の値を変更するとわかりにくくなる。 - 自身が監視されていないと動作しない(他のオブジェクトを監視しない)。
そこで Flow
に変換して、ViewModel.viewModelScope
のスコープで監視するようにします。
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
コマンドとメッセンジャー
最後に、ログインとキャンセルの処理を実装します。
これらは MVVM では本来「コマンド」3として View 層から呼び出せるように公開するべきものです。
Android には「コマンド」の仕組みがないため、単なる関数として公開します。
これらの関数を実行した結果は返値ではなく「メッセンジャーパターン」を使って発信します。
コマンドに相当する関数はレイアウトファイルでボタン押下などに割り当てられるため、返値を返しても View 層では処理できないからです。
また、ログインとキャンセルの結果である「ログインに成功した」「ログインに失敗した」「キャンセルされた」は「状態」ではなくある瞬間の出来事なので、状態を表すオブジェクトである LiveData
を使うのは適切ではありません。
メッセンジャーパターンは、「メッセンジャー」オブジェクトを介してメッセージ(情報)を送受信するデザインパターンです。
送信側はメッセンジャーにメッセージを渡し、受信側はメッセンジャーを監視することでメッセージを受け取ります。
Observer パターンの一種ですね。
メッセンジャーオブジェクトには SharedFlow
クラスが適しています。
監視を開始する際に渡した CoroutineScope
がアクティブでなくなれば自動的に監視が解除されるので、監視の解除忘れによるメモリリークが起こらないからです。
メッセージオブジェクトには sealed クラスを使うのがよいでしょう。
サブクラスごとに全く異なる情報を持たせられ、
すべてのサブクラスが既知なので受信側でキャストして
サブクラスごとに異なる処理に回すことができます。
(このあたりについては受信側である View 層を実装する際に見ていきます。)
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()
}
}
View 層
View 層の役割
View 層は UI コンポーネントに依存した処理を受け持ちます。
ViewModel 層の説明でも述べましたが、
View 層では UI コンポーネント同士の連携を実装してはいけません。
それは ViewModel 層の仕事です。
Android では Activity
と Fragment
は UI コンポーネント(View
クラス)に依存しますので View 層になります。
コードビハインド
View 層からはできる限り「コードビハインド」を減らすべきです。
コードビハインドというのは手続き型の言語(Kotlin や Java など)によるコードのことです。
宣言型の言語(XML など)によるコードはコードビハインドではありません。
宣言的に実装することでコードの見通しがよくなります。
レイアウトファイルでレイアウトを実装し、
レイアウトファイル上で UI コンポーネントの属性と ViewModel
オブジェクトのプロパティとのデータバインディングを行うことで、
コードビハインドを大幅に減らすことができます。
レイアウトファイル
ではレイアウトファイルを作成しましょう。
<!-- 本質でない属性は省略 -->
<?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、パスワードは、ユーザーによって変更されたときに View 層から ViewModel 層にも伝える必要があるため、双方向データバインディングします。
各 UI コンポーネントの有効/無効の指定は ViewModel 層から View 層への一方向でよいため単方向データバインディングです。
ボタンがクリックされたときの処理では viewModel
オブジェクトが持つ関数を呼び出しています。
コンバーター
どのラジオボタンが選択されているかは RadioButtonGroup
要素の android:checkedButton
属性で指定しますが、
その値は RadioButton
オブジェクトのリソース ID です。
これは View 層にあるレイアウトファイルで定義された値であるため、
ViewModel 層で使ってはいけません。
そこで ViewModel 層からは公開するプロパティは UserType
型(の値を持つ MutableLiveData
)にして、
それを View 層でコンバーターを使って @IdRes Int
型に変換しています。
<!-- 省略 -->
<!-- 本質でない属性は省略 -->
<RadioGroup
android:id="@+id/radio_group_user_type"
android:checkedButton="@={UserTypeToRadioButtonIdResConverter.convert(viewModel.userType)}">
<!-- 省略 -->
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
クラスを作成します。
まず、バインディングの設定を行います。
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
}
そしてメッセージの受信開始と、受信したときの処理を実装します。
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 を用いた実装の方法を紹介しました。
各層の役割と層同士の依存関係を守って、見通しのよいコードにしていきましょう。
誤りなどがありましたらご指摘いただければ幸いです。
参考
/以上
-
ソフトウェアアーキテクチャの1つ。有名な MVC から派生したもの。 ↩
-
実際の開発ではそう都合よくは行きませんが。 ↩
-
ここでは詳細は述べません。興味があれば次の記事をご覧ください。https://www.atmarkit.co.jp/fdotnet/chushin/greatblogentry_02/greatblogentry_02_03.html ↩