LoginSignup
4
1

More than 3 years have passed since last update.

Hilt環境でのDefaultViewModelProviderFactoryの実装

Last updated at Posted at 2021-02-22

3行まとめ

  • @DefaultActivityViewModelFactory および@DefaultFragmentViewModelFactory のQualifierを利用してマルチバインディングすると、ViewModelProvider.Factoryを指定しない形でViewModelを生成した際に使用される DefaultViewModelProviderFactoryの実装が可能である。
  • @HiltViewModel をViewModelに付与すると、ViewModelのパッケージ名をKeyとしたMap<String, ViewModel>型のオブジェクトグラフが登録される。
  • HiltViewModelFactoryでは、@HiltViewModelが付いたViewModelに対しては、内部で定義されたHiltViewModelFactoryが使用され、それ以外のViewModelに対しては、今回指定するViewModelProviderFactorySavedStateViewModelFactory()が使用される。

検証環境

com.google.dagger:hilt-android:2.32-alpha"

実装Sample

はじめに

Hilt環境でのDefaultViewModelProviderFactory

Hiltでは、Activity/Fragmentに@AndroidEntryPointを追記すると、AppCompatActivity/Fragmentと該当のActivity/Fragmentクラスの間に、Hilt_〇〇Activity/Fragmentが自動生成される。

UserActivity.kt
@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 に差し替えている。

Hilt_UserActivity.kt
// ↓↓↓ 自動生成された中間ファイル
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ライブラリ側に任せる事が出来なくなる。

UserViewModel.kt
@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の実装

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生成のカスタマイズを行う。
            }
        }
    }
}

FragmentへのDefaultViewModelProviderFactoryの実装

ViewModelProviderModule.kt
@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へのDefaultViewModelProviderFactoryActivityComponentに配置する。

DefaultViewModelFactories.java
/** 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の内容を追っていく。

Hilt_UserActivity.kt
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getActivityFactory(this);
  }

まずgetActivityFactory() の実装内容を覗いてみると、以下の様にActivityEntryPoint経由で、ActivityからInternalFactoryFactoryを取得し、InternalFactoryFactory#fromActivity()を実行する事でHiltViewModelFactory を生成している。
※このInternalFactoryFactoryの実装が、HiltViewModelFactory作成の要所となるので、詳細に触れていく。

DefaultViewModelFactories.java
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();
  }
DefaultViewModelFactories.java
/** 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);
    }
}

まず、上記コード内の以下の部分だが、

DefaultViewModelFactories.java
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を取得する処理が実行されている。

EntryPoints.java
/** 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の自動生成ファイル)の中身を覗いてみる。

DaggerApp_HiltComponents_SingletonC.java
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をインスタンス化しているだけである。

DefaultViewModelFactories.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);
    }
}

そして、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を付与すると、

UserViewModel.kt
@HiltViewModel
class UserViewModel @Inject constructor(
    private val userManager: UserManager
) : ViewModel()

ViewModelのパッケージ情報から、@HiltViewModelMap.KeySet というQualifierでパケージ名の文字列がマルチバインディングされ、ViewModel本体も、パッケージ名をStringKeyとして@HiltViewModelMapというQualifierでMap<String, ViewModel> の形でオブジェクトグラフに登録される。

UserViewModel_HiltModules.kt
  @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作成まで一気に追っていく。

DefaultViewModelFactories.java
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() の実装は以下の様になっており、

DefaultViewModelFactories.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; // 第五引数
// (中略)
    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の作成が行われる。

HiltViewModelFactory.kt
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がどうなっているかを確認すると、以下の様になっている。

HiltViewModelFactory.kt
@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の作成を行い、含まれなかった場合は、実装方法で、マルチバインディングによって指定した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の最後に示した様に、@DefaultActivityViewModelFactory / @DefaultFragmentViewModelFactoryは、@AndroidEntryPointをつける以前に実装可能であったHasDefaultViewModelProviderFactory. getDefaultViewModelProviderFactory() を置き換えるものであり、HiltViewModelFactoryによって生成されるViewModelの生成に対する共通処理を定義できるものでは無い。
その為、@HiltViewModelが付いたViewModelに対しては、ComponentActivity.viewModels()Fragment.viewModels() に対するExtensionを定義するなどして、共通処理を定義する事が必要となる。

@DefaultActivityViewModelFactoryを使用しなかった場合の@HiltViewModelの付かないViewModelに対するインスタンス化について

本筋とは外れるので、HiltViewModelFactoryのインスタンス作成では触れなかったが、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;

 

4
1
0

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
4
1