はじめに
みなさま、値の受け渡しはどうしてますか?
私は去年までベタベタな組み込みC言語ユーザで、戻り値、参照渡しくらいしか使っていませんでした
当然、非同期的なやりとりをしようとすると詰みます
ということで私なりに整理したものを共有したいと思います
テーマは、「どのようにしてModelからViewModelに通知すべきか」、です
さて、Androidで一般的なMVVMアーキテクチャでは、アプリケーションはおよそ3層で表現できます
この時、ViewModelからActivityへの通知はLiveDataを使うのが公式です
LiveDataには通常のObserverに加えて、Activityのライフサイクルを与えられるので、
ViewModelが値を返すときには既にActivityが破棄されていているかもしれない」という心配がありません
LiveData便利ですね
ですが、Model - ViewModel間ではどうしましょうか?
LiveDataを使おうにも、与えるべきLifecycleOwnerはActivityやFragmentしか持っていません
というところが本記事のはじまりです
サンプルプロジェクト
お題はこちら
- Repositoryで、別スレッドを立て、その中で変数を0,1,2,...とカウントアップする
- ViewModelはModelからの(なんらかの)通知でそれを知る
- ActivityはViewModelをLiveDataで監視して、値を画面に表示する
いろんなやり方でModelからViewModelに通知してみた
まずは基本のコールバックだ!
class MainRepository(private val callback: (Int)-> Unit) { // callbackを登録
private var count = 0
init {
object: Thread() {
override fun run() {
Log.e("Repository", " ${currentThread()}")
for (i in 0..10) {
sleep(1000)
count = i
callback.invoke(count) // カウントアップごとに通知
}
}
}.start()
}
}
一方で受け取る側のViewModelはこんな実装です
class MainViewModel : ViewModel() {
lateinit var model : MainModel
val state = MutableLiveData<Int>()
init {
state.value = 0
model = MainModel(
callback = { // ここでModelから通知された時の処理をここに書く
state.postValue(it)
})
}
}
非常にシンプルでわかりやすいですね
ただし、一つのModelが複数のViewModelや他Modelに通知したい場合は厳しいかもしれません
個人的には2層以上コールバックが入れ子になると極端に読みにくくなるような気もします
ちょっと強引にLiveDataを使ってみる
LifecycleOwnerを実装して、LiveDataのActive状態をコントロールする
こちらの記事を参考に、ViewModelにもLifecycleOwnerを実装してしまえばLiveDataも使えます
なければ作ればいいのです
(なお、しっかりした動作確認はしていませんので悪しからず)
class MainModel(){
val count = MutableLiveData<Int>(0)
init {
object: Thread() {
override fun run() {
for (i in 0..10) {
sleep(1000)
count.postValue(i)
}
}
}.start()
}
}
Model側はViewModelと同様にLiveDataを使う実装にします
その一方で、ViewModel側には独自のLifecycleOwnerを定義した上で、ModelをLiveData的に監視します
class MainViewModel : ViewModel() {
val state = MutableLiveData<Int>()
val model = MainModel()
private val lifecycleOwner = CustomLifecycleOwner()
init {
lifecycleOwner.start()
state.value = 0
model.count.observe(lifecycleOwner, Observer {
state.postValue(it) // 実はここは常にメインスレッドで呼ばれる
})
}
override fun onCleared() {
lifecycleOwner.stop()
super.onCleared()
}
inner class CustomLifecycleOwner : LifecycleOwner {
private val lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle = lifecycleRegistry
fun start() {
lifecycleRegistry.currentState = Lifecycle.State.STARTED
}
fun stop() {
lifecycleRegistry.currentState = Lifecycle.State.CREATED
}
}
}
これでやりたいことはできるのですが、ちょっぴり気をつける必要があることがあります
実はLiveDataで通知されるコールバックはメインスレッドで実行されるようです
なので多用するとアプリのパフォーマンスを下げるらしく、RepositoryでLiveDataを使うなと言っている人もいたりします
なんだか無理矢理感もあるので、できるならSharedViewModelで回避したいところかなと思います
そしてStateFlowへ...
先ほど挙げたRepositoryでLiveDataを使うな派の人が推奨している方法です
- コード量はコールバック並に少ない
- LiveDataと同じようなことができる ※厳密には色々違います
- setValueとpostValueを区別する必要がない
- 監視する側で、どのスレッド・スコープで動かすかを指定できる
と、メリットが盛りだくさんです
class MainModel() {
val count = MutableStateFlow(0)
init {
object: Thread() {
override fun run() {
for (i in 0..10) {
sleep(1000)
count.value = i
}
}
}.start()
}
}
class MainViewModel : ViewModel() {
val model = MainModel()
init {
state.value = 0
viewModelScope.launch(Dispatchers.IO) { // コルーチンを別スレッドで実行する
model.count.collect {
state.postValue(it)
}
}
}
}
しかしながら、学習コストは高めだと思います
このサンプルではそれほどコード量も多くないですが、前提としてコルーチンを理解している必要がありそうです
例えば以下のような点がわかってないと、導入は難しそうかなと
- LiveDataとは通知の仕様がやや異なる
- 注意して使わないとリソースを浪費する可能性がある
- 既存のライブラリと連携するときにはcallbackFlowを使うなど、応用が必要な場面がある
(私はまだわからないので遠慮中)
まとめ
- 1対1なら、シンプルにコールバックにする
- 1対多なら、まずはSharedViewModelを使うことを検討する
- それでもダメならLiveDataを使う (しかし控えめに)
- チーム全体で学習コストを払えるのなら、StateFlowを使う
というところでしょうか...
ぜひぜひ他にも良いやり方がありましたらお教えください!