旨辛チキンおいしい

よくある備忘録みたいな奴です。

Android Architecture Component の ViewModel と画面遷移

MVVM において View の責任は、 ViewModel が表す画面の状態を実際に画面に描画をすることだけであるべきだと思う。

つまり View は、自分自身の意思で画面遷移を行ってはいけない。

ややこしいのは、 Android では View はたいてい Activity が実装するということだ。

Android で画面遷移をちゃんと行える1のは Activity のみなので、 View を実装しているクラスが画面遷移も行わなければならないことになる。

さらに困ったことに、 Android Architecture Component の ViewModel は Activity を間接的にでも参照してはならない2

ViewModel は Configuration Changes に対応するために Activity のライフサイクルを超えたライフサイクルを持つので、たしかに Activity を保持してしまうと Configuration Changes によって再生成される前の古い Activity を保持し続けてしまうことになり、 Activity 全体を簡単にリークさせてしまう。

View は画面遷移に責任を持つべきではない。 ViewModel の指示で画面遷移が行いたい。でも ViewModel が参照してはいけない Activity しか画面遷移を行えない。困った。

AACGoogle Samples の GitHub issue3 ではこれに対して1つの解決策が提示されている。

その解決策は、 ViewModel が画面遷移などが必要なときに値を通知するような LiveData を公開しておき、 Activity がそれを監視して画面遷移などを行うという方法だ。

こんな感じで実装できそうだ:

sealed class Page {
    class MainPage : Page()
    class ListPage : Page()
    class DetailPage(val item: Item) : Page()
}

object Navigator {
    fun bind(activity: AppCompatActivity, navigatable: Navigatable) {
        navigatable.navigationEvents.observe(activity, Observer { page ->
            when (page) {
                is Page.MainPage -> Intent(activity, MainActivity::class.java)
                is Page.ListPage -> Intent(activity, ListActivity::class.java)
                is Page.DetailPage -> Intent(activity, DetailActivity::class.java).also {
                    it.putExtra("item", page.item)
                }
            }?.let {
                activity.startActivity(it)
            }
        })
    }
}

interface Navigatable {
    val navigationEvents: LiveData<Page>
}

abstract class BaseViewModel : ViewModel(), Navigatable {

    private val _navigationEvents = MutableLiveData<Page>()

    override val navigationEvents: LiveData<Page>
        get() = _navigationEvents
}

class MainViewModel : BaseViewModel() {
    // something
}

class MainActivity : AppCompatActivity() {
    fun onCreate() {
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        Navigator.bind(this, viewModel)
    }
}

class ListViewModel : BaseViewModel() {
    // something
}

class ListActivity : AppCompatActivity() {
    fun onCreate() {
        val viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
        Navigator.bind(this, viewModel)
    }
}

しかし個人的にはこの方法はなにか違う気がする。

ViewModel が View の状態以外の情報を通知し、それを Activity が購読するという構成がなぜか腑に落ちない。

どうしても ViewModel から Navigator (上のサンプルとは異なる) に直接指示を出すようにして、 Activity で Navigator を実装したくなる。

そのほうが「ViewModel の LiveData = View の情報を通知するもの」「Navigator は画面遷移に責任を持つもの」「View としての Activity は ViewModel の LiveData を監視して画面を更新」「Navigator としての Activity は画面遷移を実装する」という感じで役割がきれいにわかれる気がする。

ViewModel に WeakReference で Activity (やそれに依存するオブジェクト) の参照を保持するという方法もありそうだったけど、それで大丈夫なのかどうかまだわかってないのでとりあえず保留


  1. Application などの Context で startActivity することもできるが、 Activity の Back Stack などと相性が悪い。

  2. ViewModel Overview  |  Android DevelopersCaution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context. と書いてある。

  3. View actions (snackbar, activity navigation, …) in ViewModel · Issue #63 · googlesamples/android-architecture-components · GitHub