ちょっと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がいるようですね。
上記のViewModelを追加したときのViewModel Componentの図です。一体何が起こっているんでしょうか?
以下のように利用しているとしましょう。
@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を紐付けているようですね。
@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