はじめに
こんにちは、Androidエンジニアのどすこいです。
今回はタイトルにある通り、ViewPager2で共通のViewModelを使う方法を解説したいと思います。
タイトルだけ見て、なんのこっちゃと思うかもしれないのでまずは次の図をご覧ください
こんな感じで親FragmentがViewPager2を用いて3つの子Fragmentを持っている状況があるとします。
その際、全ての子Fragmentで同じViewModelを使いたい時にどうするべきか悩んだので共有します。
環境
Android Architecture Componentを使用します。
appレベルのbuild.gradleに次の記述をしてください。
dependencies {
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
}
今回説明しないこと
ViewPager2の使い方
Android Architecture Componentのこと
ViewModelの生成方法
まずViewModelの生成方法から解説していきます。
ActivityスコープでのViewModelの生成方法
private val viewModel: HogeViewModel by activityViewModels()
これでActivityスコープでのViewModelが生成できます。
Activity内で複数ViewModelを生成しないのであればこれでいいと思います。
しかしActivityの責務が大きければ複数のViewModelを生成することが多いでしょう。
その場合、そのActivityが死なない限りViewModelも破棄されません。そうなるとメモリリークの可能性も大きくなるので、ActivityスコープでのViewModelの生成はお勧めしません。
FragmentスコープでのViewModelの生成方法
private val viewModel: HogeViewModel by viewModels()
これでFragmentスコープでのViewModelが生成されます。
基本的にViewModelを生成する場合はこの方法がお勧めです。
画面遷移をするとFragmentが死ぬのでそれに伴いViewModelも破棄されメモリリークを起こすことはないでしょう。
ただこの方法を今回のViewPager2を使った方法で用いると何が起こるでしょうか?
子Fagment1のスコープでViewModelを生成するとします。
子Fragment1の状態をViewModelに保存します。(入力した値など)
ViewPager2を用いてFragmentを変更した場合、子Fragement2で再びViewModelを生成し子Fragment1で生成したViewModelが破棄されます。
なので子Fragment1に戻った際、先ほどの状態を子Fragment1に反映できません。
それを解決すべく、ViewModelStoreとViewModelStoreOwnerという概念を使用していきます。
ViewModelStoreOwner
筆者もViewModelStoreOwnerは完全に理解しているわけではないので、間違い等あればご指摘ください。
ViewModelStoreとViewModelStoreOwnerとは
ViewModelStoreはViewModelを保存してくれるクラスです。
ViewModelStoreOwnerはgetViewModelStoreというViewModelStoreを返すメソッドのみが存在しているインターフェイスです。
ViewModelStoreはViewModelStoreOwner#getViewModelStore()を呼び出した時にActivityやFragmentに対応して生成されていて、既に対応したViewModelStoreが存在する場合はそれを返却するようになっています。
まずこれらを理解するためにViewModelを生成するときに使ったactivityViewModels()やviewModels()が何をやっているか見ていきましょう。
@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })
@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}
裏側ではこんなことをしてくれてるんですね。
正直数ヶ月前までは裏側のコードを見てると頭が痛くなってましたが、最近では少し慣れてきてちょっとずつ読むようになりました(笑)
まずactivityViewModels()の方はそれを呼び出したFragmentの親ActivityのViewModelStoreをcreateViewModelLazy()に渡してくれています。
viewModels()の方は少し違っていますね。
ViewModelStoreOwnerを渡さない場合は、thisを使ってViewModelsStoreをcreateViewModelLazy()に渡してくれています。このthisというのは呼び出したFragmentのことです。
ViewPager2を使った場合、このViewModelStoreOwnerに親Fragmentを渡してあげれば親Fragmentが死なない限り、同じインスタンスのViewModelが使えるということになります!
ViewPager2で共通のViewModelを使う方法
さて本題です!
まずは親Fragmentで共通して使いたいViewModelを生成します。
private val viewModel: HogeViewModel by viewModels()
そして子Fragmentで親FragmentのViewModelStoreOwnerを用いて同じViewModelを生成してあげます
private val viewModel: HogeViewModel by viewModels(
{ parentFragment!! }
)
こうしてあげることでそれぞれの子Fragmentで共通したViewModelが生成できますね。
まとめ
本題より前提知識や説明の方が長くなってしまいましたね(笑)
Android Architecture Componentはまだ発表されてそこまで時間が経っていないので、詳しく紹介している記事が少なかったり見つけたとしても英語でよくわからん、みたいなことが多々あって苦労するところはあると思います(少なくとも筆者はめっちゃ苦労してる)
なので今後も詳しく紹介する記事や、初心者向けの記事を書いていけたらなぁなんて思ってるので今後もよろしくお願いします!
参考にした記事
ViewModel, ViewModelProviderについて調べてみた