ViewModelの初期化方法について整理してみた。
Android開発においてViewModelはほぼ必須であるが、ViewModelの初期化方法が新旧も含め色々あるので整理してみた。
公式ホームページ
ViewModel の概要
ViewModelに引数がない場合
class MyViewModel : ViewModel() {
・・・
}
ViewModelに引数がない場合はby viewModelsをつかえばいい、。Acrivity、Fragmentでも一緒
private val myViewModel: MyViewModel by viewModels
ViewModelに引数がある場合
引数がある場合はViewModelにFactoryクラスが必要になる。
class MyViewModel(private val repository: MyRepository): ViewModel() {
・・・
}
class MyViewModelFactory(private val repository: MyRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw java.lang.IllegalArgumentException("Unknown ViweModel class")
}
}
Activity,Fragment側は初期化の際にFactoryクラスとその引数を追加する。
private val myViewModel: MyViewModel by viewModels {
MyViewModelFactory(MyRepository())
}
ActivityとFragament、Fragment間でviewModelを共有したい場合。
画面を跨いで、ActivityとFragament、Fragment間でviewModelを共有したい場合がある。その場合はby viewModels()の代わりにby activityViewModels()を使う。
private val viewModel: SharedViewModel by activityViewModels()
viewModelに引数がある場合は、上記と同様である。
古いviewModelの初期化の仕方、色々
viewModelも色々歴史があるので、ネットではまだ古い初期化の呼び方が散見される。中には廃止予定になっている呼び方もあるので注意が必要。
- androidx.lifecycle.ViewModelProvider
- androidx.lifecycle.ViewModelProviders 廃止予定
- android.arch.lifecycle.ViewModelProvider 廃止予定
- android.arch.lifecycle.ViewModelProviders 廃止予定
パッケージがandroidとandroidxのもの、ViewModelProviderかViewModelProvidersで後ろに「s」が付くかつかないかで4つあって、非常に紛らわしい。
パッケージがandroidxの方が現在使用可能で、「x」が付かないのは廃止予定となっている。
ViewModelProvidersはViewModelStoreのユーティリティクラスとなっている。
結局、4つのうち下の3つは廃止予定、上の1つだけが現在少可能となっている。
Android KTXが出る前までは
ViewModelProvider(this).get(UserModel.class)
の呼び方が使われているのが多いが、今はby viewModels、 by activityViewModelsを使ったほうが良さそうである。
結局、ViewModelProvider(this).get(UserModel.class)とby viewModelsは同じなのか?
by viewModels は kotlin の委譲を使っているだけで、同じだと思う。(自信なし)
ViewModelの初期化の引数について
ViewModelはAACでもViewModelの中からRepositoryクラスを呼び出すようになっているので、コンストラクタの引数にRepositoryクラスの引数を持つのが普通である。
class MyViewModel(private val repository: MyRepository): ViewModel() {
・・・
}
今、このような構成を考えてみる。画面は全てFragmentで画面遷移する。MainActivity は初期でLoginFragmentを表示する。ログイン完了するとMainFragmentに遷移する。MainFragment と ViewFragmentは行ったり来たりする。MyViewModelはActivityと全てのFragmentで共有する。
- MainActivity (by viewModels で MyViewModel を初期化)
- LoginFragment (by activityViewModels で MyViewModel を初期化)
- MainFragment (by activityViewModels で MyViewModel を初期化)
- ViewFragment (by activityViewModels で MyViewModel を初期化)
この場合、全てのクラスでMyViewModelの初期化に引数は必要だろうか?
ViewModelの特性を考えてみると、ViewModelの生存期間はActivityよりも長い。画面が回転した時Activityは廃棄され新たに作られる。それでもViewModelはインスタンスを保持している。
このViewModelのインスタンスを保持しているのがViewModelStoreクラスである。
by viewmodelあるいはby activityViewModelsが呼ばれた場合、ViewModelStoreクラスにそのViewModelのインスタンスがあればそれを返す。なければ初期化して作成し、ViewModelStoreクラスに保持する。
すなわち、viewmodelあるいはby activityViewModelsの引数が使われるのは、ViewModelStoreクラスにインスタンスがない最初の1回目だけである。
あとはViewModelStoreクラスにあるインスタンスを返しているだけなので引数を書いても使われない。(じゃないと、ActivityとFragment間で共有できない。毎回初期化してたら中身が保持されない。)
上記の場合、MainActivityが必ず最初に呼ばれるとわかっているので、引数が必要なのはMainActivityだけである。
Fragmentだけで、どのFragmentが先によばれるのかわからない場合は全てのFragmentで引数を書く必要がある。(上のようにLoginFragmentが必ず最初とわかっているのであれば、そこだけでいい)