一発ネタです。ViewModelをkey指定で取得するとき、同じkeyで異なるViewModelのインスタンスを取得しようとするとはまります。必ずモデルクラスごとにkeyが異なるように名前空間に注意が必要というお話。
要約
通常ViewModelを取得するときは、以下のようにViewModelProviderから取得します。拡張関数のviewModels()
なども最終的には同じことをやっています。
val hogeViewModel = ViewModelProvider(this).get(HogeViewModel::class.java)
val fugaViewModel = ViewModelProvider(this).get(FugaViewModel::class.java)
通常、各ViewModelはインスタンスが複数必要なことはないのでこれで良いですが、例えば、ViewPagerの各Fragment用のViewModel等は、FragmentスコープではなくActivityスコープにする必要があり、ページごとに異なるインスタンスをActivityのViewModelStoreに保持してもらう必要があります。そのような場合、getにkeyを指定して、
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(this).get(key, HogeViewModel::class.java)
のように使います。
さて、このFragmentでViewModelがもう一つ必要になった場合、
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(requireActivity()).get(key, HogeViewModel::class.java)
val fugaViewModel = ViewModelProvider(requireActivity()).get(key, FugaViewModel::class.java)
としてはいけません。
例えば以下のように、keyがモデルクラスごとに異なる値となるような工夫が必要です。
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(requireActivity())
.get(HogeViewModel::class.qualifiedName + ":" + key, HogeViewModel::class.java)
val fugaViewModel = ViewModelProvider(requireActivity())
.get(HogeViewModel::class.qualifiedName + ":" + key, FugaViewModel::class.java)
なぜか
ソースコードを追ってみましょう。
private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey";
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
viewModel = mFactory.create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
このように、keyを指定しない場合DEFAULT_KEY + ":" + canonicalName
をkeyとしてgetがコールされています。
keyを指定したgetの中身を見てみると、mViewModelStoreからkeyを使って、ViewModelを取り出し、モデルクラスとクラスが一致しているかを確認しています。ViewModelStoreはHashMapのラッパーの用なもので、一つのkeyに一つのインスタンスしか保持できません。
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
...
return (T) viewModel;
クラスが異なる場合、その後の処理に流れ、新規でViewModelのインスタンスが作られ、ViewModelStoreにkeyを使って格納されます。当然、そのkeyで格納されていた別のViewModelは上書きされ保持されなくなります。
elseの分岐で// TODO: log a warning.
とあるように、エラーログを出す予定らしいですね。
つまり、冒頭で書いたように
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(requireActivity()).get(key, HogeViewModel::class.java)
val fugaViewModel = ViewModelProvider(requireActivity()).get(key, FugaViewModel::class.java)
ということをすると、最初にHogeViewModelが作られ、FugaViewModelで上書きされ、次回も同様にと、毎回ViewModelのインスタンスが作り直されることになるため、意図と異なる挙動になってしまいます。
keyを指定しない場合は、内部でクラス名がkeyに使われているので、モデルクラスが異なれば別のkeyで格納され、問題無く動作するだけに、挙動をよく理解しないで使ってしまうとはまってしまいます。
ViewModelは複数使わないということであれば単純なkeyでもかまわないかもしれませんが、少なくとも複数つかう必要がある場合は、keyにモデルクラス名を追加するなどして、名前空間が分かれるように工夫する必要があります。
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(requireActivity())
.get(HogeViewModel::class.qualifiedName + ":" + key, HogeViewModel::class.java)
val fugaViewModel = ViewModelProvider(requireActivity())
.get(HogeViewModel::class.qualifiedName + ":" + key, FugaViewModel::class.java)
さすがにこれは冗長なので、拡張関数などをつくってシンプルにするのが良いでしょう。
以上です。