はじめに
2021年12月に公式のアーキテクチャガイドが大幅に更新されました。
この記事では、UI層の大まかな解説と、ViewModelイベントについて、LiveDataとFlowの2パターンでの実装例を示しながら解説を行います。
UI層の大まかな解説
公式のアーキテクチャガイドをおさらいしてみると、 UI層、Domain層、Data層
の3つに分かれており、それぞれ一方向に依存している
ことが分かります。
この一方向に依存しているというのが、後から重要になってくるので覚えておいてください。
さらに、UI層はViewを表示するUI elemntsと状態管理を行う State holders
に分かれていることが分かります。
このStateholderについて少し掘り下げて解説を行います。
State holdersとは
State holdersは状態変数を管理するレイヤー
のことであり、状態変数をState holdersに集めることで、状態管理をスッキリと書くことができるようになります。
data class UiState(
// APIからの返り値を格納
val data: APIResult?,
// eventを格納
val events: List<Event> = emptyList(),
// loadingを格納
val proceeding: Boolean
)
sealed interface Event {
object Success : Event
data class Error(val message: String) : Event
object NextPage: Event
}
このように状態変数を1つにまとめることで、以下のように 1つの変数のみ
で状態管理を行うことができるようになります。
private val _uiState = MutableStateFlow(UiState(//初期値))
private val _uiState = MutableLiveData(UiState(//初期値))
val uiState: LiveData<UiState>
get() = _uiState
ViewModelイベント
UIの基本構造を理解したところで、続いて大きく変更のあったViewModelイベントについて解説を行います。
ViewModelイベントとは、ViewModel側からUIの操作を行う
ことを意味しており、 UIの状態やライフサイクルに沿ってUIを更新する必要がある
という注意点があります。
ゆえに、UIの状態を意識せずにコードを書くと以下のようなAntiPatternに遭遇してしまいます。
Anti Pattern
APIからErrorを受け取ったときにダイアログを表示する
という例を用いて、ViewModelイベントでのAntiPatternを紹介します。
以下のような流れで実装をしてみたとします。
APIからErrorを受け取る → LiveDataにErrorの変数を格納する → LiveDataを監視して、ダイアログを表示する
実装例
以下のようなSealedClassを用意してErrorの変数を定義し、
// 結果を返すクラス
sealed class APIResult<out R> {
// 成功した場合
data class Success<out T>(val data: T) : APIResult<T>()
// 失敗した場合
data class Error(val exception: Throwable) : APIResult<Nothing>()
}
ViewModel側で以下のようなLiveDataにErrorを格納し、
// Errorを格納するLiveData
private val _result = MutableLiveData<APIResult?>(null)
val Result: LiveData<APIResult?>
get() = _weather
これを監視して、Errorが生じていたときにダイアログを表示するという実装です。
// LiveDataを監視
viewModel.result.observe(this) {
// エラーが生じていた場合 -> エラー画像を表示
is APIResult.Error -> {
// ダイアログを表示する
showNoticeDialog()
Log.e("API fetch error", weather.exception.toString())
}
このような実装は一見問題がないように見えますが、横画面にすると、ダイアログが2回重なって表示
されてしまうという問題が生じてしまいます。
なぜこんなことが起きたのか
実はこの実装では、横画面になった時にErrorの変数がもう一度参照されてしまうために、ダイアログが再生成されてしまうのです。
詳しい解説をすると、横画面になったときに以下のようなライフサイクルを辿るため、2回ダイアログが表示されてしまうようです。
onSaveInstanceState() → Viewの状態を保存 (ここで以前表示されていたダイアログが保存される)
onDestroy() → Viewを破棄
onCreate() → Viewを生成(LiveDataに格納された変数を参照するため、新しくダイアログが生成される)
→ ダイアログを1個表示
onStart()
onRestoreInstanceState() → 保存した状態を復元(保存されたダイアログが復元される)
→ ダイアログを2個表示してしまった…
onResume()
このような状態を防ぐためには、ダイアログを表示するというViewModelの変数を、ダイアログが表示された後に破棄する(消費する)
ような実装が必要になってきます。
イベント管理方法のBetterパターン
イベントを消費する方法について解説を行います。
かつての方法(現在は未推奨)
以前に推奨されていたイベント管理の方法は、このDeveloperブログによると
Eventクラスを用意して
open class Event<out T>(private val content: T) {
// イベントが消費されているかどうかフラグ
var hasBeenHandled = false
private set // 外から値の変更はできない
// Eventがラップする値がまだ一度も消費されていない場合にnon-nullな値を返す関数
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
}
Event型でViewModelイベントをラップし、
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
UIでgetContentIfNotHandledの関数を行うことでイベントの消費を行うというものでした。
// APIResultが来たときに発火する処理
viewModel.navigateToDetails.observe(this) {
it.getContentIfNotHandled()?.let { APIResult ->
handleWeatherUpdate(APIResult)
}
}
イベントを消費する関数をEvent型の中に定義することで、適切に変数を破棄するという方法をとっています。
問題点
しかし、この方法では UIelements→Stateholders という単方向の依存が成り立たなくなってしまう
という問題が生じてしまいます。(少し前までは許容されてたからややこしいよね…)
そこで、以下のような方法が提唱されています。
現時点でのBestパターン
公式の方法をさらにスッキリとまとめた記事がabe kenjiさんのブログ記事で紹介されているため、この記事を参考に現時点でのBestパターンを紹介します。
実装の流れ
以下のような流れでViewModelイベントを扱っていきます。
- 状態をSealed interfaceで意義する
- UIStateでEventを格納するListを定義する
- イベントを消費する関数を定義する
- イベントを発行する
- View側からイベントを消費する
Flowでの実装例
上の流れに沿って実装例を紹介します。
(Flowのコードはスタぜろさんの記事からの引用です)
1. 状態を定義する
まず、ViewModelイベントで扱う状態を定義します。
sealed interface Event {
object Success : Event
data class Error(val message: String) : Event
}
2. UIStateでEventを格納するListを定義する
Listに変数を格納することがポイント
data class UiState(
// APIからの返り値を格納
val data: APIResult?,
// イベントを格納(ここ重要)
val events: List<Event> = emptyList(),
// ローディング画面を制御
val proceeding: Boolean
)
3. ViewModelでEventを保持する
UIState内にまとめて1つの変数で保持するというのが定石でしたね(上で解説しました)
private val _uiState = MutableStateFlow(UiState(//初期値))
val uiState: StateFlow<UiState> = _uiState
4. イベントを消費する関数をViewModel内に定義する
イベントを消費する関数をViewModel内に定義する
というのが単方向の依存においてポイントになります。
fun consumeEvent(event: Event) {
val newEvents = uiState.events.filterNot { it == event }
uiState = uiState.copy(events = newEvents)
}
4. ViewModelイベントを発行する
copy関数を用いてイベントの発行を行います。
// APIから正しい返り値があった場合
val newEvents = uiState.events + Event.Success
_uiState.value.copy(events = newEvents, data = result.data)
// Errorが生じた場合
val newEvents = _uiState.value.events + (Event.Error(result.exception.toString()))
_uiState.value.copy(events = newEvents)
5. View側からイベントを消費する
View上でUIを更新した後に消費を行うことがViewModelEventの肝となる部分です。
viewModel.uiState.events.firstOrNull()?.let { event ->
when (event) {
is Event.Success -> {
LaunchedEffect(event) {
snackbarHostState.showSnackbar("Success")
// イベントを消費
viewModel.consumeEvent(event)
}
}
LiveData
上で紹介した方法をLiveDataに落とし込むと以下のようになる。
ただし、初期値を代入できないためにnull対応がめんどくさくなっているので、StateFlowでの実装をおすすめします。
1. 状態を定義する
// Flowと同様のため省略
2. UIStateでEventを格納するListを定義する
// Flowと同様のため省略
3. ViewModel内でEventを保持する
private val _uiState = MutableLiveData(UiState(//初期値)
val uiState: LiveData<UiState>
get() = _uiState
3. イベントを消費する関数を定義する
fun consumeEvent(event: Event) {
val newEvents = _uiState.value?.events?.filterNot { it == event }
_uiState.value = uiState.value?.copy(events = newEvents ?: emptyList())
}
4. イベントを発行する
// APIから正しい返り値があった場合
val newEvents = _uiState.value?.events?.plus(Event.Success)
_uiState.value?.copy(events = newEvents ?: emptyList(), data = result.data)
// Errorが生じた場合
val newEvents = _uiState.value?.events?.plus(Event.Error(result.exception.toString()))
_uiState.value?.copy(events = newEvents ?: emptyList())
5. View側からイベントを消費する
viewModel.uiState.observe(this) { uiState ->
if (uiState.events.firstOrNull() != null) {
when (val event = uiState.events.firstOrNull()) {
is Event.Success -> {
// イベントを消費
viewModel.consumeEvent(event)
}
is Event.Error -> {
..
}
まとめ
- UI層はUI elemntsと状態管理を行う
State holders
に分かれている。 - State holdersに状態変数をまとめることで、状態管理をスッキリと行うことができる。
- ViewModelイベントの取り扱いには注意が必要であり、イベントの
消費
を行う必要がある。
最後まで読んでいただきありがとうございました。
よければtwitterのフォローよろしくお願いします。
(https://twitter.com/takashiho_2)
参考文献