LoginSignup
21
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-19

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

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

val viewModelHilt: VideoPlayerHiltViewModel by viewModels()
@HiltViewModel
class VideoPlayerHiltViewModel @Inject constructor(val videoPlayer: VideoPlayer) : ViewModel() {
}

https://github.com/takahirom/hilt-sample-app のComponent図です。(arunkumar9t2/scabbardによる生成 )どうやらActivityRetainedComponentの下にViewModelのComponentがいるようですね。
image.png

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

image.png

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

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

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

public abstract class Hilt_PlayerFragment extends Fragment implements GeneratedComponentManagerHolder {

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

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

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

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

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

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

  /** 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を取得します。

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が登場します。

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

もう一度図を見てみましょう。どうやらBindsModuleがViewModelの実際のクラスとViewModelを紐付けているようですね。
image.png

  @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

@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

21
7
1

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
21
7