2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

僕たちはどのようにしてViewModelに通知すべきなのか

Last updated at Posted at 2020-12-28

はじめに

みなさま、値の受け渡しはどうしてますか?
私は去年までベタベタな組み込みC言語ユーザで、戻り値、参照渡しくらいしか使っていませんでした
当然、非同期的なやりとりをしようとすると詰みます

ということで私なりに整理したものを共有したいと思います
テーマは、「どのようにしてModelからViewModelに通知すべきか」、です

さて、Androidで一般的なMVVMアーキテクチャでは、アプリケーションはおよそ3層で表現できます
スクリーンショット 2020-12-28 8.53.15.png
この時、ViewModelからActivityへの通知はLiveDataを使うのが公式です
LiveDataには通常のObserverに加えて、Activityのライフサイクルを与えられるので、
ViewModelが値を返すときには既にActivityが破棄されていているかもしれない」という心配がありません

LiveData便利ですね
ですが、Model - ViewModel間ではどうしましょうか?
LiveDataを使おうにも、与えるべきLifecycleOwnerはActivityやFragmentしか持っていません

というところが本記事のはじまりです

サンプルプロジェクト

スクリーンショット 2020-12-28 9.04.23.png

お題はこちら

  • Repositoryで、別スレッドを立て、その中で変数を0,1,2,...とカウントアップする
  • ViewModelはModelからの(なんらかの)通知でそれを知る
  • ActivityはViewModelをLiveDataで監視して、値を画面に表示する

いろんなやり方でModelからViewModelに通知してみた

まずは基本のコールバックだ!

MainModel.kt
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はこんな実装です

MainViewModel.kt
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も使えます
なければ作ればいいのです
(なお、しっかりした動作確認はしていませんので悪しからず)

MainModel.kt
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的に監視します

MainViewModel.kt
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を区別する必要がない
  • 監視する側で、どのスレッド・スコープで動かすかを指定できる

と、メリットが盛りだくさんです

MainModel.kt
class MainModel() {
    val count = MutableStateFlow(0)

    init {
        object: Thread() {
            override fun run() {
                for (i in 0..10) {
                    sleep(1000)
                    count.value = i
                }
            }
        }.start()
    }
}
MainViewModel.kt
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を使う

というところでしょうか...
ぜひぜひ他にも良いやり方がありましたらお教えください!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?