3行まとめ
-
@DefaultActivityViewModelFactory
および@DefaultFragmentViewModelFactory
のQualifierを利用してマルチバインディングすると、ViewModelProvider.Factoryを指定しない形でViewModelを生成した際に使用されるDefaultViewModelProviderFactory
の実装が可能である。 -
@HiltViewModel
をViewModelに付与すると、ViewModelのパッケージ名をKeyとしたMap<String, ViewModel>型
のオブジェクトグラフが登録される。 -
HiltViewModelFactory
では、@HiltViewModel
が付いたViewModelに対しては、内部で定義されたHiltViewModelFactory
が使用され、それ以外のViewModelに対しては、今回指定するViewModelProviderFactory
かSavedStateViewModelFactory()
が使用される。
検証環境
com.google.dagger:hilt-android:2.32-alpha"
実装Sample
はじめに
Hilt環境でのDefaultViewModelProviderFactory
Hiltでは、Activity/Fragmentに@AndroidEntryPoint
を追記すると、AppCompatActivity/Fragmentと該当のActivity/Fragmentクラスの間に、Hilt_〇〇Activity/Fragment
が自動生成される。
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModels()
private val sampleViewModel: SampleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
userViewModel.login()
sampleViewModel.showId()
}
}
// ↓↓↓ 自動生成された中間ファイル
public abstract class Hilt_UserActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
// 中略
}
そして、この中間ファイルはHasDefaultViewModelProviderFactory.java
で定義されているgetDefaultViewModelProviderFactory
をOverrideしており、ViewModelProvider.Factoryを指定しない形でViewModelを生成した際に使用されるdefaultViewModelProviderFactory
を、Hiltライブラリ内で実装されているHiltViewModelFactory
に差し替えている。
// ↓↓↓ 自動生成された中間ファイル
public abstract class Hilt_UserActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
private volatile ActivityComponentManager componentManager;
// 中略
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this); // HiltViewModelFactoryの作成
}
}
これにより、ViewModelに対して@HiltViewModel
を付与するだけでViewModelに対するInjectをHilt経由で行えるのだが、この恩恵を受ける事ができる一方で、Hiltの中間ファイルがgetDefaultViewModelProviderFactory
をOverrideしてしまっている為、@AndroidEntryPoint
を追記する以前には可能であったDefaultViewModelProviderFactory
のカスタマイズの両方を行う事ができなくなっている。
※もちろんActivity/Fragment側で、更にgetDefaultViewModelProviderFactory
をOverrideする事でカスタマイズは可能だが、その場合、ViewModelへのHilt経由でのInjectを、Hiltライブラリ側に任せる事が出来なくなる。
@HiltViewModel
class UserViewModel @Inject constructor(
private val userManager: UserManager
) : ViewModel()
そこで本記事では、Hilt側で用意されている@DefaultActivityViewModelFactory
および@DefaultFragmentViewModelFactory
の両Qualifierを利用して、下記の両方を実現する方法を記載する。
-
@HiltViewModel
のついたViewModelへのHilt経由でのInject -
@AndroidEntryPoint
を追記する以前には可能であった(@HiltViewModel
の付かないViewModelに対する)DefaultViewModelProviderFactory
のカスタマイズ
実現方法
時間の無い方の為に先に結論を記載しておくと、HiltのDefaultViewModelProviderFactory
では、Daggerのマルチバインディング機能を使用している為、以下の様にViewModelProviderModule
を定義する事で、DefaultViewModelProviderFactory
の指定が可能となる。
ActivityへのDefaultViewModelProviderFactoryの実装
@Module
@InstallIn(ActivityComponent::class)
object ViewModelProviderModule {
@Provides
@IntoSet
@DefaultActivityViewModelFactory
@Suppress("UNCHECKED_CAST")
fun provideDefaultActivityViewModelFactory(): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
// ここでViewModel生成のカスタマイズを行う。
}
}
}
}
FragmentへのDefaultViewModelProviderFactoryの実装
@Module
@InstallIn(ActivityComponent::class) // ActivityComponentに配置する点だけ注意
object ViewModelProviderModule {
@Provides
@IntoSet
@DefaultFragmentViewModelFactory
@Suppress("UNCHECKED_CAST")
fun provideDefaultFragmentViewModelFactory(): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
// ここでViewModel生成のカスタマイズを行う。
}
}
なおFragmentの方のコメントにも記載しているが、Hiltライブラリ内のDefaultViewModelFactories. ActivityModule
においては、ActivityComponent
に対して、@DefaultActivityViewModelFactory
と@DefaultFragmentViewModelFactory
のマルチバインディングが登録されているので、FragmentへのDefaultViewModelProviderFactory
もActivityComponent
に配置する。
/** The activity module to declare the optional factories. */
@Module
@InstallIn(ActivityComponent.class)
interface ActivityModule {
@Multibinds
@HiltViewModelMap.KeySet
abstract Set<String> viewModelKeys();
@Multibinds
@DefaultActivityViewModelFactory
Set<ViewModelProvider.Factory> defaultActivityViewModelFactory();
@Multibinds
@DefaultFragmentViewModelFactory
Set<ViewModelProvider.Factory> defaultFragmentViewModelFactory();
}
DefaultViewModelFactoriesの実装詳細
なぜ、上記ViewModelProviderModule
の定義だけで実現可能なのか?
ここからは、中間ファイルによって生成されるgetDefaultViewModelProviderFactory()
の実装詳細にも触れつつ、Hiltライブラリ内のHiltViewModelFactoryの実装を紹介していく。
(少し長くなるがお付き合いいただきたい)
HiltViewModelFactoryの取得
以降では、@AndroidEntryPointのついたActivityの場合を例として、中間ファイルがOverrideした以下のメソッド内で使用されているDefaultViewModelFactories.java
の内容を追っていく。
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this);
}
まずgetActivityFactory()
の実装内容を覗いてみると、以下の様にActivityEntryPoint
経由で、ActivityからInternalFactoryFactory
を取得し、InternalFactoryFactory#fromActivity()
を実行する事でHiltViewModelFactory
を生成している。
※このInternalFactoryFactoryの実装が、HiltViewModelFactory
作成の要所となるので、詳細に触れていく。
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
return EntryPoints.get(activity, ActivityEntryPoint.class)
.getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
.fromActivity(activity); //HiltViewModelFactory(=ViewModelProvider.Factory)の生成
}
// (中略)
@EntryPoint
@InstallIn(ActivityComponent.class)
public interface ActivityEntryPoint {
InternalFactoryFactory getHiltInternalFactoryFactory();
}
/** Internal factory for the Hilt ViewModel Factory. */
public static final class InternalFactoryFactory {
// (中略)
@Nullable private final ViewModelProvider.Factory defaultActivityFactory;
// (中略)
ViewModelProvider.Factory fromActivity(ComponentActivity activity) {
return getHiltViewModelFactory(activity,
activity.getIntent() != null ? activity.getIntent().getExtras() : null,
defaultActivityFactory);
}
}
まず、上記コード内の以下の部分だが、
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
return EntryPoints.get(activity, ActivityEntryPoint.class)
.getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
// (中略)
}
EntryPoints.get
の処理内容の実態は、第一引数に指定されたActivity/Fragmentなどから、自動生成された中間ファイルの所持するComponentManager
を取得し、そのComponentManager
の保持するComponent
を、第二引数で指定されたActivityEntryPoint
に型変換する事である。
つまり、自動生成されるActivityComponent
からInternalFactoryFactory
を取得する処理が実行されている。
/** Static utility methods for accessing objects through entry points. */
public final class EntryPoints {
@Nonnull
public static <T> T get(Object component, Class<T> entryPoint) {
if (component instanceof GeneratedComponent) {
return entryPoint.cast(component);
} else if (component instanceof GeneratedComponentManager) {
return entryPoint.cast(((GeneratedComponentManager<?>) component).generatedComponent());
} else {
throw new IllegalStateException();
}
}
}
EntryPoint経由でのInternalFactoryFactory
の取得を深堀してみる
流石に、ActivityComponent
からInternalFactoryFactory
を取得する部分は、実例が無いと分かりにくいので、インターフェースであるActivityEntryPoint
を具体化しているActivityCImpl(Hiltの自動生成ファイル)
の中身を覗いてみる。
private final class ActivityCImpl extends App_HiltComponents.ActivityC {
@Override
public DefaultViewModelFactories.InternalFactoryFactory getHiltInternalFactoryFactory() {
return DefaultViewModelFactories_InternalFactoryFactory_Factory.newInstance(
ApplicationContextModule_ProvideApplicationFactory.provideApplication(DaggerApp_HiltComponents_SingletonC.this.applicationContextModule),
keySetSetOfString(),
new ViewModelCBuilder(),
defaultActivityViewModelFactorySetOfViewModelProviderFactory(),
defaultFragmentViewModelFactorySetOfViewModelProviderFactory()
);
}
}
ここで、DefaultViewModelFactories_InternalFactoryFactory_Factory.newInstance
という部分では、Daggerから特定のインスタンスを取得する際に使用される自動生成のメソッドを呼び出しており、その実態は、以下で定義されているDefaultViewModelFactories#InternalFactoryFactory
のコンストラクタを、Daggerに登録されているオブジェクトグラフの情報を元に呼び出している処理となる。
※Daggerの処理に関しては本記事のスコープ外な為、詳細は割愛するが、要はDaggerの登録情報を元にDefaultViewModelFactories#InternalFactoryFactory
をインスタンス化しているだけである。
/** 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);
}
}
そして、InternalFactoryFactory
のコンストラクタInjectionの定義と、自動生成されたActivityCImpl
の両方をみてみると、InternalFactoryFactory
のインスタンスは
引数 | 引数名:型 | 指定される情報 |
---|---|---|
第1引数 | application : Application | Hiltが配布するApplicationContextをApplication型にキャストして指定 |
第2引数 |
@HiltViewModelMap.KeySet keySet : Set |
@HiltViewModel が付いたViewModelのパケージ名のSetが指定される(後述で補足) |
第3引数 | viewModelComponentBuilder : ViewModelComponentBuilder | DefaultFragmentViewModelFactoryが要求される際に、new ViewModelCBuilder() で新規インスタンスが生成される |
第4引数 |
@DefaultActivityViewModelFactory defaultActivityFactorySet : Set |
実装方法で、マルチバインディングによって指定したViewModelProvider.FactoryのSet |
第5引数 |
@DefaultFragmentViewModelFactory defaultFragmentFactorySet : Set |
(上記に同じく) |
という情報を元にインスタンス化される。
(補足) @HiltViewModel
を付与した際の自動生成ファイル
第3引数に関する補足だが、Hiltでは、下記の様にViewModelに @HiltViewModel
を付与すると、
@HiltViewModel
class UserViewModel @Inject constructor(
private val userManager: UserManager
) : ViewModel()
ViewModelのパッケージ情報から、@HiltViewModelMap.KeySet
というQualifierでパケージ名の文字列がマルチバインディングされ、ViewModel本体も、パッケージ名をStringKeyとして@HiltViewModelMap
というQualifierでMap<String, ViewModel>
の形でオブジェクトグラフに登録される。
@Module
@InstallIn(ViewModelComponent.class)
public abstract static class BindsModule {
@Binds
@IntoMap
@StringKey("com.akitoshi.hashizume.hiltviewmodelsample.viewmodel.UserViewModel")
@HiltViewModelMap
public abstract ViewModel binds(UserViewModel vm);
}
@Module
@InstallIn(ActivityRetainedComponent.class)
public static final class KeyModule {
private KeyModule() {
}
@Provides
@IntoSet
@HiltViewModelMap.KeySet
public static String provide() {
return "com.akitoshi.hashizume.hiltviewmodelsample.viewmodel.UserViewModel";
}
}
HiltViewModelFactoryのインスタンス作成
山場であるInternalFactoryFactory
の取得までを紹介できたので、最後に、HiltViewModelFactory作成まで一気に追っていく。
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
return EntryPoints.get(activity, ActivityEntryPoint.class)
.getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
.fromActivity(activity); //ViewModelProvider.Factoryの生成
}
DefaultViewModelFactories#getActivityFactory()
では、InternalFactoryFactoryの取得をした後にInternalFactoryFactory#fromActivity()
を実行しているが、InternalFactoryFactory#fromActivity()
の実装は以下の様になっており、
/** 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; // 第五引数
// (中略)
ViewModelProvider.Factory fromActivity(ComponentActivity activity) {
return getHiltViewModelFactory(activity,
activity.getIntent() != null ? activity.getIntent().getExtras() : null,
defaultActivityFactory);
}
// (中略)
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);
}
最終的には、以下の情報を元に、HiltViewModelFactory
のインスタンスが作成される。
引数 | 引数名:型 | 指定される情報 |
---|---|---|
第1引数 | owner | Activity/FragmentなどのSavedStateRegistryOwner |
第2引数 | defaultArgs | Activity/Fragmentから取得したBundle |
第3引数 | keySet |
@HiltViewModel が付いたViewModelのパケージ名のSetが指定される |
第4引数 | delegate | 実装方法で、マルチバインディングによって指定したViewModelProvider.FactoryのSetの「最初」の要素 |
第5引数 | viewModelComponentBuilder |
ViewModelCBuilder() の新規インスタンス |
HiltViewModelFactory
の実装内容
次に、HiltViewModelFactory
の実装内容を追っていく。
まず、HiltViewModelFactory
コンストラクタの実装は以下の様になっており、引数の情報から、hiltViewModelFactory
の作成が行われる。
public HiltViewModelFactory(
@NonNull SavedStateRegistryOwner owner, // 上の表で示したインスタンス作成時の第一引数
@Nullable Bundle defaultArgs, // 第二引数
@NonNull Set<String> hiltViewModelKeys, // 第三引数
@NonNull ViewModelProvider.Factory delegateFactory, // 第四引数
@NonNull ViewModelComponentBuilder viewModelComponentBuilder // 第五引数
) {
this.hiltViewModelKeys = hiltViewModelKeys;
this.delegateFactory = delegateFactory;
this.hiltViewModelFactory =
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 @HiltViewModel-annotated class '"
+ modelClass.getName()
+ "' to be available in the multi-binding of "
+ "@HiltViewModelMap but none was found.");
}
return (T) provider.get();
}
};
}
そして、肝心のHiltViewModelFactory
が実装しているViewModelProvider.Factory
がどうなっているかを確認すると、以下の様になっている。
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
// hiltViewModelKeysは、`@HiltViewModel`が付いたViewModelのパケージ名のSet
if (hiltViewModelKeys.contains(modelClass.getName())) {
return hiltViewModelFactory.create(modelClass);
} else {
//delegateFactoryは、今回マルチバインディングによって指定したViewModelProvider.Factory
return delegateFactory.create(modelClass);
}
}
```
つまり、作成されようとしているViewModelのクラス名が、`@HiltViewModel`が付いたViewModelのパケージ名のSetに含まれる場合は、HiltによるInjectが要求されているとして、コンストラクタで作成した`hiltViewModelFactory`経由でViewModelの作成を行い、含まれなかった場合は、[実装方法](https://qiita.com/AkitoshiHashizume/private/44a0bc62235ba5e68d6a#%E5%AE%9F%E7%8F%BE%E6%96%B9%E6%B3%95)で、マルチバインディングによって指定したViewModelProvider.Factoryが使われる事となる。
※長くなったが、これで、下記の`ViewModelProviderModule`を定義する事で、DefaultViewModelProviderFactoryの実装の実装が可能な事が確認された。
```ViewModelProviderModule.kt
@Module
@InstallIn(ActivityComponent::class)
object ViewModelProviderModule {
@Provides
@IntoSet
@DefaultActivityViewModelFactory
@Suppress("UNCHECKED_CAST")
fun provideDefaultActivityViewModelFactory(): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
// ここでViewModel生成のカスタマイズを行う。
}
}
}
}
```
# 注意
##### Hilt経由でInjectされるViewModelに対するDefaultViewModelProviderFactoryの実装
[Hilt環境でのDefaultViewModelProviderFactory](https://qiita.com/AkitoshiHashizume/private/44a0bc62235ba5e68d6a#hilt%E7%92%B0%E5%A2%83%E3%81%A7%E3%81%AEdefaultviewmodelproviderfactory)の最後に示した様に、@DefaultActivityViewModelFactory / @DefaultFragmentViewModelFactoryは、@AndroidEntryPointをつける以前に実装可能であった`HasDefaultViewModelProviderFactory. getDefaultViewModelProviderFactory()` を置き換えるものであり、`HiltViewModelFactory`によって生成されるViewModelの生成に対する共通処理を定義できるものでは無い。
その為、@HiltViewModelが付いたViewModelに対しては、`ComponentActivity.viewModels()`や`Fragment.viewModels()` に対するExtensionを定義するなどして、共通処理を定義する事が必要となる。
##### @DefaultActivityViewModelFactoryを使用しなかった場合の@HiltViewModelの付かないViewModelに対するインスタンス化について
本筋とは外れるので、[HiltViewModelFactoryのインスタンス作成](https://qiita.com/AkitoshiHashizume/private/44a0bc62235ba5e68d6a#hiltviewmodelfactory%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E4%BD%9C%E6%88%90)では触れなかったが、`InternalFactoryFactory.getHiltViewModelFactory()`内の処理をみると、マルチバインディングによって`ViewModelProvider.Factory`を指定しない場合は、`SavedStateViewModelFactory`のインスタンスが使用されている。
※つまり引数無しのコンストラクタによってインスタンス化される。
その為、引数を要求するViewModelなどをインスタンス化するときには、この記事で紹介した@DefaultActivityViewModelFactoryや@DefaultFragmentViewModelFactoryを指定する必要がある。
```DefaultViewModelFactories.java
//extensionDelegateは、マルチバインディングによって指定したViewModelProvider.FactoryのSet要素 or 指定が無い場合はnull
//変数delegateは、HiltViewModelFactoryで使用される@HiltViewModelの付かないViewModelに対するViewModelFactory
ViewModelProvider.Factory delegate = extensionDelegate == null
? new SavedStateViewModelFactory(application, owner, defaultArgs)
: extensionDelegate;
```