HiltViewModelでAbstractなViewModelをInjectしてみるチャレンジ

ちょっとHiltViewModelで何ができて何ができないのかわからないので調べてみましょう。

HiltViewModelは基本的にはこういう感じで利用できます。

```kotlin
val viewModelHilt: VideoPlayerHiltViewModel by viewModels()
```

```kotlin
@HiltViewModel
class VideoPlayerHiltViewModel @Inject constructor(val videoPlayer: VideoPlayer) : ViewModel() {
}
```


https://github.com/takahirom/hilt-sample-app のComponent図です。(arunkumar9t2/scabbardによる生成 )どうやらActivityRetainedComponentの下にViewModelのComponentがいるようですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/552d45ec-dc90-1f06-76b8-91332a5cf6d7.png)

上記のViewModelを追加したときのViewModel Componentの図です。一体何が起こっているんでしょうか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/4ba9dd55-f5ba-d78c-f68a-f435053cf8cd.png)

以下のように利用しているとしましょう。

```kotlin
@AndroidEntryPoint
class PlayerFragment : Fragment(R.layout.fragmenet_player) {
    val viewModelHilt: VideoPlayerHiltViewModel by viewModels()
```


Hiltは以下のようなクラスを生成して、それをsuperクラスとして利用します。どうやらデフォルトのFactoryがいるようです。by viewModels()はこのFacotryをデフォルトで利用しています。

```java
public abstract class Hilt_PlayerFragment extends Fragment implements GeneratedComponentManagerHolder {

....
@Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getFragmentFactory(this);
  }
}
```

DefaultViewModelFactories.getFragmentFactoryはどうなっているでしょうか？


```java
  public static ViewModelProvider.Factory getFragmentFactory(Fragment fragment) {
    return EntryPoints.get(fragment, FragmentEntryPoint.class)
        .getHiltInternalFactoryFactory()
        .fromFragment(fragment);
  }
```

どうやらInternalFactoryFactoryというのをDaggerのComponentから作るようです。

```java
  @EntryPoint
  @InstallIn(FragmentComponent.class)
  interface FragmentEntryPoint {
    InternalFactoryFactory getHiltInternalFactoryFactory();
  }
```

InternalFactoryFactoryのコンストラクタには `@Inject`がついており、Daggerが自動的にインスタンスを作ることができます。
基本的にはHiltViewModelFactoryを作るだけです。

```java
  /** Internal factory for the Hilt ViewModel Factory. */
  public static final class InternalFactoryFactory {

    private final Application application;
    private final Set<String> keySet;
    private final ViewModelComponentBuilder viewModelComponentBuilder;
    @Nullable private final ViewModelProvider.Factory defaultActivityFactory;
    @Nullable private final ViewModelProvider.Factory defaultFragmentFactory;

    @Inject
    InternalFactoryFactory(
            Application application,
        @HiltViewModelMap.KeySet Set<String> keySet,
        ViewModelComponentBuilder viewModelComponentBuilder,
        // These default factory bindings are temporary for the transition of deprecating
        // the Hilt ViewModel extension for the built-in support
        @DefaultActivityViewModelFactory Set<ViewModelProvider.Factory> defaultActivityFactorySet,
        @DefaultFragmentViewModelFactory Set<ViewModelProvider.Factory> defaultFragmentFactorySet) {
      this.application = application;
      this.keySet = keySet;
      this.viewModelComponentBuilder = viewModelComponentBuilder;
      this.defaultActivityFactory = getFactoryFromSet(defaultActivityFactorySet);
      this.defaultFragmentFactory = getFactoryFromSet(defaultFragmentFactorySet);
    }

    ViewModelProvider.Factory fromActivity(ComponentActivity activity) {
      return getHiltViewModelFactory(activity,
          activity.getIntent() != null ? activity.getIntent().getExtras() : null,
          defaultActivityFactory);
    }

    ViewModelProvider.Factory fromFragment(Fragment fragment) {
      return getHiltViewModelFactory(fragment, fragment.getArguments(), defaultFragmentFactory);
    }

    private ViewModelProvider.Factory getHiltViewModelFactory(
        SavedStateRegistryOwner owner,
        @Nullable Bundle defaultArgs,
        @Nullable ViewModelProvider.Factory extensionDelegate) {
      ViewModelProvider.Factory delegate = extensionDelegate == null
          ? new SavedStateViewModelFactory(application, owner, defaultArgs)
          : extensionDelegate;
      return new HiltViewModelFactory(
          owner, defaultArgs, keySet, delegate, viewModelComponentBuilder);
    }

    @Nullable
    private static ViewModelProvider.Factory getFactoryFromSet(Set<ViewModelProvider.Factory> set) {
      // A multibinding set is used instead of BindsOptionalOf because Optional is not available in
      // Android until API 24 and we don't want to have Guava as a transitive dependency.
      if (set.isEmpty()) {
        return null;
      }
      if (set.size() > 1) {
        throw new IllegalStateException(
            "At most one default view model factory is expected. Found " + set);
      }
      ViewModelProvider.Factory factory = set.iterator().next();
      if (factory == null) {
        throw new IllegalStateException("Default view model factory must not be null.");
      }
      return factory;
    }
  }
```


HiltViewModelFactoryは以下です。基本的に`ViewModelFactoriesEntryPoint`からViewModelのProviderを取得します。


```java
public final class HiltViewModelFactory implements ViewModelProvider.Factory {

  @EntryPoint
  @InstallIn(ViewModelComponent.class)
  interface ViewModelFactoriesEntryPoint {
    @HiltViewModelMap
    Map<String, Provider<ViewModel>> getHiltViewModelMap();
  }

  /** Hilt module for providing the empty multi-binding map of ViewModels. */
  @Module
  @InstallIn(ViewModelComponent.class)
  interface ViewModelModule {
    @Multibinds
    @HiltViewModelMap
    Map<String, ViewModel> hiltViewModelMap();
  }

  private final Set<String> viewModelInjectKeys;
  private final ViewModelProvider.Factory delegateFactory;
  private final AbstractSavedStateViewModelFactory viewModelInjectFactory;

  public HiltViewModelFactory(
      @NonNull SavedStateRegistryOwner owner,
      @Nullable Bundle defaultArgs,
      @NonNull Set<String> viewModelInjectKeys,
      @NonNull ViewModelProvider.Factory delegateFactory,
      @NonNull ViewModelComponentBuilder viewModelComponentBuilder) {
    this.viewModelInjectKeys = viewModelInjectKeys;
    this.delegateFactory = delegateFactory;
    this.viewModelInjectFactory =
        new AbstractSavedStateViewModelFactory(owner, defaultArgs) {
          @NonNull
          @Override
          @SuppressWarnings("unchecked")
          protected <T extends ViewModel> T create(
              @NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle) {
            ViewModelComponent component =
                viewModelComponentBuilder.savedStateHandle(handle).build();
            Provider<? extends ViewModel> provider =
                EntryPoints.get(component, ViewModelFactoriesEntryPoint.class)
                    .getHiltViewModelMap()
                    .get(modelClass.getName());
            if (provider == null) {
              throw new IllegalStateException(
                  "Expected the @ViewModelInject-annotated class '"
                      + modelClass.getName()
                      + "' to be available in the multi-binding of "
                      + "@ViewModelInjectMap but none was found.");
            }
            return (T) provider.get();
          }
        };
  }

  @NonNull
  @Override
  public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    if (viewModelInjectKeys.contains(modelClass.getName())) {
      return viewModelInjectFactory.create(modelClass);
    } else {
      return delegateFactory.create(modelClass);
    }
  }
}
```

そしてHiltViewModelMapが登場します。


```java
  @EntryPoint
  @InstallIn(ViewModelComponent.class)
  interface ViewModelFactoriesEntryPoint {
    @HiltViewModelMap
    Map<String, Provider<ViewModel>> getHiltViewModelMap();
  }
```

もう一度図を見てみましょう。どうやらBindsModuleがViewModelの実際のクラスとViewModelを紐付けているようですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/4ba9dd55-f5ba-d78c-f68a-f435053cf8cd.png)

```java
  @Module
  @InstallIn(ViewModelComponent.class)
  public abstract static class BindsModule {
    @Binds
    @IntoMap
    @StringKey("com.github.takahirom.hiltsample.VideoPlayerHiltViewModel")
    @HiltViewModelMap
    public abstract ViewModel binds(VideoPlayerHiltViewModel vm);
  }
```

このようにすることで、ViewModelのインスタンスとHiltを結びつけているようです。

**つまり、、AbstractなViewModelをInjectできるのでは。。？そして、AssitedInjectも共存できるのでは。。？これによってテストとかで置き換えたりとかできたりするんじゃ？？**
**結論からいうとできましたが、`@StringKey`でクラス名を指定したりなど、結構自動生成でない書き方は厳しい感じで、しかも渡したい引数もFragmentのインスタンスなどがないと参照できないため、ViewModelのスコープでは参照できないため、あまり意味がありませんでした。**
https://github.com/takahirom/hilt-sample-app/commit/a93fd366fb6929a927bfc936c0d71fdace4738af

```kotlin
@AndroidEntryPoint
class PlayerFragment : Fragment(R.layout.fragmenet_player) {
    private val videoPlayerViewModel: AbstractVideoPlayerViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        videoPlayerViewModel.play()
    }
}

@Module
@InstallIn(ViewModelComponent::class)
abstract class BindsModule {
    @Binds
    @IntoMap
    @StringKey("com.github.takahirom.hiltsample.AbstractVideoPlayerViewModel")
    @HiltViewModelMap
    abstract fun binds(viewModel: AbstractVideoPlayerViewModel): ViewModel
}


@Module
@InstallIn(ActivityRetainedComponent::class)
object KeyModule {
    @Provides
    @IntoSet
    @HiltViewModelMap.KeySet
    fun provide(): String {
        return "com.github.takahirom.hiltsample.AbstractVideoPlayerViewModel"
    }
}

@Module
@InstallIn(ViewModelComponent::class)
class FactoryModule {
    @Provides
    fun provide(factory: VideoPlayerViewModel.Factory): AbstractVideoPlayerViewModel {
        return factory.create("?????")
    }
}

abstract class AbstractVideoPlayerViewModel : ViewModel() {
    abstract fun play()
    abstract fun isPlaying(): Boolean
}

class VideoPlayerViewModel @AssistedInject constructor(
    private val videoPlayer: VideoPlayer,
    @Assisted private val videoId: String
) : AbstractVideoPlayerViewModel() {
...
```

書いた後に知ったのですが、Assited Inject + HiltViewModelは以下で議論中みたいで、もしかすると後々実装されたりするかもです。
https://github.com/google/dagger/issues/2287