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

一発ネタです。ViewModelをkey指定で取得するとき、同じkeyで異なるViewModelのインスタンスを取得しようとするとはまります。必ずモデルクラスごとにkeyが異なるように名前空間に注意が必要というお話。

# 要約

通常ViewModelを取得するときは、以下のようにViewModelProviderから取得します。拡張関数の`viewModels()`なども最終的には同じことをやっています。

```kotlin
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を指定して、

```kotlin
val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString()
val hogeViewModel = ViewModelProvider(this).get(key, HogeViewModel::class.java)
```

のように使います。
さて、このFragmentでViewModelがもう一つ必要になった場合、

```kotlin
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がモデルクラスごとに異なる値となるような工夫が必要です。

```kotlin
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)
```

# なぜか

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

```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に一つのインスタンスしか保持できません。

```java
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
    ...
    return (T) viewModel;
```

クラスが異なる場合、その後の処理に流れ、新規でViewModelのインスタンスが作られ、ViewModelStoreにkeyを使って格納されます。当然、そのkeyで格納されていた別のViewModelは上書きされ保持されなくなります。

elseの分岐で`// TODO: log a warning.`とあるように、エラーログを出す予定らしいですね。

つまり、冒頭で書いたように

```kotlin
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にモデルクラス名を追加するなどして、名前空間が分かれるように工夫する必要があります。

```kotlin
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)
```

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

以上です。