1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ViewModelをkey指定で取得するときはkeyの名前空間に注意しよう

Posted at

一発ネタです。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)

なぜか

ソースコードを追ってみましょう。

ViewModelProvider.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)

さすがにこれは冗長なので、拡張関数などをつくってシンプルにするのが良いでしょう。

以上です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?