はじめに
KoinでViewModelをインジェクトする際には、viewModel()
とsharedViewModel()
の二つのやり方がある。
僕が担当しているアプリでは基本Fragment-ViewModelで一対一の関係になっているのだけど、ViewPagerを使用していたりするとViewModelをFragment間で共有したい場面が多くて、そういう時にsharedViewModel()
が大活躍してくれる。
ただ、今までなんとなくsharedViewModel()
を使ってきてしまったが故に、こないだちょっと沼にハマってしまった。
今回はそんなsharedViewModel()
で共有したViewModelのスコープについてご紹介。
sharedViewModelとは?
前述の通り、ViewModelをFragment間で共有できるViewModelをインジェクトする。
と、今まで思っていた。
というのも実はこのsharedViewModel()
、インスタンスを共有するスコープがデフォルトでActivityになっているらしい。
中の実装をチラッと覗いてみると、こんな感じになっている。
/**
* Lazy getByClass a viewModel instance shared with Activity
*
* @param qualifier - Koin BeanDefinition qualifier (if have several ViewModel beanDefinition of the same type)
* @param from - ViewModelStoreOwner that will store the viewModel instance. Examples: "parentFragment", "activity". Default: "activity"
* @param parameters - parameters to pass to the BeanDefinition
*/
inline fun <reified T : ViewModel> Fragment.sharedViewModel(
qualifier: Qualifier? = null,
noinline from: ViewModelStoreOwnerDefinition = { activity as ViewModelStoreOwner },
noinline parameters: ParametersDefinition? = null
): Lazy<T> = kotlin.lazy { getSharedViewModel<T>(qualifier, from, parameters) }
見て欲しいのは、第二引数のfromのところ。
コメントでViewModelStoreOwner that will store the viewModel instance. Examples: "parentFragment", "activity". Default: "activity"
とある通り、デフォルトではViewModelのインスタンスを保存している場所がactivityとなっている。
そのため、「ViewModelをFragment間で共有したい時に使用する」という認識は誤りで、**「Activity内で共有できるViewModelをインジェクトする」**という認識の方が正しかったりする。
スコープを正しく認識していないことによる問題
sharedViewModel()
のスコープをきちんと認識できていなかったことがわかったわけだけども、これが原因であるバグが発生していた。
それが、下の画像のパターン。
こんな感じでFragmentA
にFragmentB
が乗っている画面を作りたくて、二つのFragmentでViewModelを共有するためにsharedViewModel()
を使用していた。
で、この画面が実は下の画像のように、同じ画面に遷移する導線を持っていた。
この時ViewModelは画面遷移の際に新たに生成される、と思っていた。
実はこのViewModel、sharedViewModel()
を使っているせいで、画面遷移をしても同じインスタンスのViewModelが使用されている。
自分のプロジェクトはSingleActivityで作られているので、基本的にアプリが起動している間ずっとViewModelのインスタンスが破棄されない状態になっていた。
このままでは、前のデータが残っていたり無駄にインスタンスが残っていたりしてしまう。
解決方法
解決方法はそんなに難しくない。
ViewModelを保持しておくスコープを、各Fragmentにしてあげればいいだけ。
さっき見たsharedViewModel()
の第二引数のデフォルト引数が{ activity as ViewModelStoreOwner }
となっていたので、こいつを変えてあげる。
private val viewModel: ViewModel by sharedViewModel()
こいつを
private val viewModel: ViewModel by sharedViewModel(from = { requireParentFragment() })
こう!
まとめ
SingleActivityでアプリを作っている場合は、スコープをActivityにしたい場面ってあんまりないと思うので、もしかしたら基本parentFragmentを指定してあげるのがいいのかもしれない。
Daggerもそうだけど、DIは設定が難しくてなんとなく使っちゃいがちだけど、やっぱりこういうのきちんとドキュメント読まなきゃだめだね。
おわり。