LoginSignup
115
83

More than 5 years have passed since last update.

Architecture Components を Dagger2 と併用する際の ViewModelProvider.Factory について

Last updated at Posted at 2018-01-19

はじめに

こちらは以下のスライドの前半部分の補足記事となっています。
スライドについても合わせて見ていただければ幸いです。

Kotlin × Architecture Components 実践
https://speakerdeck.com/satorufujiwara/kotlin-x-architecture-components

これまでArchitecture ComponentsとDagger2を組み合わせた実装については、以下のような記事で紹介してきました。
本記事はその中のViewModelProvider.Factoryにフォーカスした記事です。

Dagger2の導入やDagger2以外の実装についてはそれぞれの記事をご覧ください。

また、この記事は以下のバージョンのArchitecture Componentsを用いて書かれています。

build.gradle
implementation "android.arch.lifecycle:runtime:1.0.3"
implementation "android.arch.lifecycle:extensions:1.0.0"
kapt "android.arch.lifecycle:compiler:1.0.0"

参考リポジトリ

Architecture ComponentsとDagger2を組み合わせる理由

まずはArchitecture ComponentsとDagger2を併用する理由と、その際に理解しておきたい部分について解説します。

ViewModelのインスタンスはViewModelProvider経由で取得する

class MainViewModel : ViewModel()

上記のようなViewModelを継承したMainViewModelというクラスがある場合、このクラスのインスタンスを取得する際は以下のようにします。

MainActivity.kt
val viewModelProvider: ViewModelProvider = ViewModelProviders.of(this)
//ViewModelを継承したクラスを指定する
val viewModel: MainViewModel = viewModelProvider.get(MainViewModel::class.java)

この際、ViewModelProviders::ofから取得したViewModelProviderクラスの関数getからインスタンスを取得する必要があります。
上記はActivity内の例でしたが、Fragment内で取得する場合は以下のような挙動になります。

MainFragment.kt
viewModelA = ViewModelProviders.of(activity!!).get(MainViewModel::class.java)
// viewModelAとは別のインスタンス
viewModelB = ViewModelProviders.of(this).get(MainViewModel::class.java)

viewModelAviewModelBは同じ型のクラスですが別のインスタンスです。
ViewModelProviders::ofに指定できる引数はFragmentActivityもしくはFragmentですが、それぞれの場合において内部的には以下のような処理が呼ばれています。

ViewModelProviders.java
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
    initializeFactoryIfNeeded(checkApplication(activity));
    return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}

public static ViewModelProvider of(@NonNull Fragment fragment) {
    initializeFactoryIfNeeded(checkApplication(checkActivity(fragment)));
    return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
}

ViewModelStoresの中はさらに以下のようになっていて、

ViewModelStores.java
public static ViewModelStore of(@NonNull FragmentActivity activity) {
    return holderFragmentFor(activity).getViewModelStore();
}

public static ViewModelStore of(@NonNull Fragment fragment) {
    return holderFragmentFor(fragment).getViewModelStore();
}

holderFragmentFor関数は以下のようなHolderFragment内のstatic関数です。

HolderFragment.java
public static HolderFragment holderFragmentFor(FragmentActivity activi
    return sHolderFragmentManager.holderFragmentFor(activity);
}

public static HolderFragment holderFragmentFor(Fragment fragment) {
    return sHolderFragmentManager.holderFragmentFor(fragment);
}

さらに辿ることでHolderFragment.HolderFragmentManagerクラスに行き着きます。
このクラス内のholderFragmentFor関数がそれぞれ呼ばれています。

HolderFragment.HolderFragmentManager
private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
    HolderFragment holder = new HolderFragment();
    fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
    return holder;
}

HolderFragment holderFragmentFor(FragmentActivity activity) {
    FragmentManager fm = activity.getSupportFragmentManager();
    HolderFragment holder = findHolderFragment(fm);
    if (holder != null) {
        return holder;
    }
    holder = mNotCommittedActivityHolders.get(activity);
    if (holder != null) {
        return holder;
    }
    if (!mActivityCallbacksIsAdded) {
        mActivityCallbacksIsAdded = true;
        activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
    }
    holder = createHolderFragment(fm);
    mNotCommittedActivityHolders.put(activity, holder);
    return holder;
}

HolderFragment holderFragmentFor(Fragment parentFragment) {
    FragmentManager fm = parentFragment.getChildFragmentManager();
    HolderFragment holder = findHolderFragment(fm);
    if (holder != null) {
        return holder;
    }
    holder = mNotCommittedFragmentHolders.get(parentFragment);
    if (holder != null) {
        return holder;
    }
    parentFragment.getFragmentManager()
            .registerFragmentLifecycleCallbacks(mParentDestroyedCallback, false);
    holder = createHolderFragment(fm);
    mNotCommittedFragmentHolders.put(parentFragment, holder);
    return holder;

このコードで注目したいのは、FragmentActivityの場合はactivity.getSupportFragmentManager()が呼ばれ、Fragmentの場合はparentFragment.getChildFragmentManager()が呼ばれているという箇所です。
それぞれの関数からは異なるFragmentManagerが取得されるので、それぞの場合において、findHolderFragment(fm)で取得できるHolderFragmentのインスタンスや、createHolderFragment(fm)で生成されるHolderFragmentのインスタンスは異なってきます。
すなわちViewModelProviders::ofの引数(FragmentActivityorFragment)から取得できるFragmentManagerが同一かによってHolderFragmentのインスタンスが同一がどうかが決まります

HolderFragmentViewModelStore型のメンバ変数mViewModelStoreを持っていて、ViewModelStoreHashMap<String, ViewModel>型のメンバ変数を用いてViewModelのインスタンスを管理しています。
このHashMap型の変数のString型のキーは以下のように決められています。

ViewModelProviders.java
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    if (canonicalName == null) {
        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    }
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

キーはViewModelProvider::getの引数に指定したクラス名で決まるので、HolderFragmentのインスタンスが同一であり、かつ、ViewModelProvider:getの引数に同一のクラス名を指定すればViewModelのインスタンスも同じになります。

まとめると、

val viewModel = ViewModelProviders.of(activity!!).get(MainViewModel::class.java)

上記のようにViewModelのインスタンスを取得する場合、

  • ViewModelProviders::ofの引数から取得できるFragmentManagerが同一であれば、ViewModelのインスタンスの格納場所(ViewModelStore)が同一になる
  • すなわち、異なるFragment同士であっても同じActivity内にあって、ViewModelProviders::ofの引数にActivityを指定すれば同じ格納場所を取得できる
  • 格納場所からViewModelを取得する際、ViewModelProvider:getの引数のクラス名がそのキーとなる

ViewModelProvider.Factoryを指定したViewModelのインスタンスの取得

先程の例だとViewModelProviders::ofの引数は1つしか指定しませんでしたが、この場合、内部的には以下のようになっています。

ViewModelProviders.java
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
    initializeFactoryIfNeeded(checkApplication(activity));
    return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}

sDefaultFactoryというのはViewModelProvider.Factoryを継承したクラスで、以下のような実装になっています。

DefaultFactory
public static class DefaultFactory extends ViewModelProvider.NewInstanceFactory {
    private Application mApplication;
    /**
     * Creates a {@code DefaultFactory}
     *
     * @param application an application to pass in {@link AndroidViewModel}
     */
    public DefaultFactory(@NonNull Application application) {
        mApplication = application;
    }
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
            //noinspection TryWithIdenticalCatches
            try {
                return modelClass.getConstructor(Application.class).newInstance(mApplication);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            }
        }
        return super.create(modelClass);
    }
}

このクラスの親となっているViewModelProvider.NewInstanceFactoryは以下のようなクラスです。

ViewModelProvider.NewInstanceFactory
public static class NewInstanceFactory implements Factory {
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        //noinspection TryWithIdenticalCatches
        try {
            return modelClass.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        }
    }
}

それぞれの実装において注目したいのは、DefaultFactoryreturn modelClass.getConstructor(Application.class).newInstance(mApplication);の部分と、ViewModelProvider.NewInstanceFactoryreturn modelClass.newInstance();の部分です。
その実装に注目すると、sDefaultFactoryを使った場合、

  • AndroidViewModelを継承したViewModelの場合、コンストラクタにApplicationクラスが引数として渡され、インスタンスが生成される
  • 上記ではないViewModelの場合、引数なしのコンストラクタが呼ばれ、インスタンスが生成される

という挙動になります。

したがって、次のViewModelクラスのように、Applicationクラス以外をコンストラクタの引数としたい場合はDefaultFactoryではない、独自のViewModelProvider.Factoryが必要になります。

MainViewModel.kt
class MainViewModel constructor(private val repository: GitHubRepository) : ViewModel() {

上記のMainViewModelクラスのインスタンスを生成できるように、独自のViewModelProvider.Factoryを作ると以下のようになります。

ViewModelFactory.java
public class ViewModelFactory extends ViewModelProvider.NewInstanceFactory {

    @SuppressLint("StaticFieldLeak")
    private static volatile ViewModelFactory INSTANCE;

    private final Application mApplication;

    private final TasksRepository mTasksRepository;

    public static ViewModelFactory getInstance(Application application) {

        if (INSTANCE == null) {
            synchronized (ViewModelFactory.class) {
                if (INSTANCE == null) {
                    INSTANCE = new ViewModelFactory(application,
                            Injection.provideTasksRepository(application.getApplicationContext()));
                }
            }
        }
        return INSTANCE;
    }

    @VisibleForTesting
    public static void destroyInstance() {
        INSTANCE = null;
    }

    private ViewModelFactory(Application application, TasksRepository repository) {
        mApplication = application;
        mTasksRepository = repository;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass.isAssignableFrom(StatisticsViewModel.class)) {
            //noinspection unchecked
            return (T) new StatisticsViewModel(mApplication, mTasksRepository);
        } else if (modelClass.isAssignableFrom(TaskDetailViewModel.class)) {
            //noinspection unchecked
            return (T) new TaskDetailViewModel(mApplication, mTasksRepository);
        } else if (modelClass.isAssignableFrom(AddEditTaskViewModel.class)) {
            //noinspection unchecked
            return (T) new AddEditTaskViewModel(mApplication, mTasksRepository);
        } else if (modelClass.isAssignableFrom(TasksViewModel.class)) {
            //noinspection unchecked
            return (T) new TasksViewModel(mApplication, mTasksRepository);
        }
        throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass.getName());
    }
}

引用元 : https://github.com/googlesamples/android-architecture/blob/todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ViewModelFactory.java

メソッドpublic <T extends ViewModel> T create(Class<T> modelClass)内の実装に注目してください。
このメソッド内で、if文による場合分けが必要になります。
また、メソッドgetInstanceで行っているように、ViewModelFactory自身をシングルトンにする工夫なども必要になります。

Applicationクラス以外をコンストラクタの引数としたい場合、このような手間を省くためにDagger2を使うことができます。

Dagger2のMultibinding機能を使ってViewModelのインスタンスを生成する

Dagger2を利用して、独自のViewModelProvider.Factoryを使う場合はMultibinding機能を使います。

ViewModelKey.kt
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

上記のようにキーを指定するためのアノテーションを定義して、Module内には以下のように記述します。

MainModule.kt
@Module
internal abstract class MainModule {
  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel::class) //Key名
  abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}

上記のように記述することで、ViewModelクラスをinjectしたい場合、MainViewModel::classをキー名とすれば、MainViewModelのインスタンスが生成されinjectされることになります。

このとき、独自のViewModelProvider.Factoryは以下のように定義します。

ViewModelFactory.kt
class ViewModelFactory @Inject constructor(
  private val creators: Map<Class<out ViewModel>,
  @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    var creator: Provider<ViewModel>? = creators[modelClass]
    if (creator == null) {
      for ((key, value) in creators) {
        if (modelClass.isAssignableFrom(key)) {
          creator = value
          break
        }
      }
    }
    if (creator == null) throw IllegalArgumentException("unknown model class " + modelClass)
    try {
      return creator.get() as T
    } catch (e: Exception) {
      throw RuntimeException(e)
    }
  }
}

コンストラクタの引数にあるMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>型のcreatorsに、MainModuleクラスの記述に応じたマップがinejectされます。

ViewModelクラスをinjectしたい場合、MainViewModel::classをキー名とすれば、MainViewModelのインスタンスが生成されinjectされることになります。

と先程書きましたが、それはすなわち、var creator: Provider<ViewModel>? = creators[modelClass]の箇所で、modelClassMainViewModel::classが指定されれば、creatorMainViewModel型を返すProviderということになります。実際に、return creator.get() as Tとして、creatorからMainViewModel型のインスタンスを取得し、キャストした上で返しています。

ここでのProviderはDagger2内のインタフェースで、Provider injectionの機能を利用するためのものです。

Provider.java
public interface Provider<T> {

    /**
     * Provides a fully-constructed and injected instance of {@code T}.
     *
     * @throws RuntimeException if the injector encounters an error while
     *  providing an instance. For example, if an injectable member on
     *  {@code T} throws an exception, the injector may wrap the exception
     *  and throw it to the caller of {@code get()}. Callers should not try
     *  to handle such exceptions as the behavior may vary across injector
     *  implementations and even different configurations of the same injector.
     */
    T get();
}

コメントにあるように、Providerは型パラメータに指定した型のインスタンスを、依存性を解決した上で返すためのクラスです。Provider Injectionについては以下の記事が参考になるかと思います。

このとき、Dagger2によって生成されるコードは以下のようになっていて、

DaggerAppComponent.java
//いろいろ省略

private MainViewModel_Factory mainViewModelProvider;

private Map<Class<? extends ViewModel>, Provider<ViewModel>>
    getMapOfClassOfAndProviderOfViewModel() {
  return Collections.<Class<? extends ViewModel>, Provider<ViewModel>>singletonMap(
      MainViewModel.class, (Provider) mainViewModelProvider);
}
private ViewModelFactory getViewModelFactory() {
  return new ViewModelFactory(getMapOfClassOfAndProviderOfViewModel());
}
MainViewModel_Factory.java
public final class MainViewModel_Factory implements Factory<MainViewModel> {
  private final Provider<GitHubRepository> repositoryProvider;

  public MainViewModel_Factory(Provider<GitHubRepository> repositoryProvider) {
    this.repositoryProvider = repositoryProvider;
  }

  @Override
  public MainViewModel get() {
    return new MainViewModel(repositoryProvider.get());
  }

  public static MainViewModel_Factory create(Provider<GitHubRepository> repositoryProvider) {
    return new MainViewModel_Factory(repositoryProvider);
  }
}

Collections.<Class<? extends ViewModel>, Provider<ViewModel>>singletonMap(MainViewModel.class, (Provider) mainViewModelProvider);ViewModelFactoryのコンストラクタ引数に渡される実態となるインスタンスで、MainViewModel.classがキー、MainViewModelを生成するmainViewModelProvider(=MainViewModel_Factory型のメンバ変数mainViewModelProvider)がその値となっていることがわかります。

Dagger2を使ったViewModelProvider.Factoryの注意点

先程のViewModelFactoryのコンストラクタの引数に@JvmSuppressWildcardsがついています。
これがないとビルドエラーとなります。
Dagger2によって生成されたJavaコードを見ればわかりますが、ViewModelFactoryのコンストラクタに代入されるcreatorMap<Class<? extends ViewModel>, Provider<ViewModel>>型(Javaでの表記)です。

creators : Map<Class<out ViewModel>, Provider<ViewModel>>

一方で、@JvmSuppressWildcardsの無い、上記のコードをコンパイルするとMap<Class<? extends ViewModel>,? extends Provider<ViewModel>>型(Javaでの表記)となります。

これに関しては以下の記事が参考になると思います。

さらに、関数createの中の実装についてですが、var creator: Provider<ViewModel>? = creators[modelClass]として、一度creatorsからProvider<ViewModel>型のcreatorを取得してから、creatornullなら再度マップ内を探索しています。
手元ではcreatornullとならなかったので、後続の処理が必要な理由がいまいち不明です。
参考となるリポジトリでも、最初のcommitからそのようになっていました。
こちらに関してもしご存知の方がいたらTwitterやコメントなどで教えてください。

SubComponetを使うかどうか

GitHubBrowserSampleGitHubViewModelFactoryには@Singletonというアノテーションがついていますが、kotlin-architecture-componentsViewModelFactoryにはついていません。
前者は文字通りアプリ内で唯一のインスタンスを持ちますが、後者はインスタンスが必要な時に都度生成をしています。
この違いはSubComponentを使うかどうかの違いによるものです。

ViewModelProvider.Factoryをシングルトンにする場合

まずは@Singletonをつけた場合についてどうなるか考えます。この場合、そのコンストラクタにinejectされるMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>型の引数creatorsもシングルトンになります。
したがってその元となるModuleの定義も以下のようにまとめて書く必要があります。

ViewModelModule.java
@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(UserViewModel.class)
    abstract ViewModel bindUserViewModel(UserViewModel userViewModel);

    @Binds
    @IntoMap
    @ViewModelKey(SearchViewModel.class)
    abstract ViewModel bindSearchViewModel(SearchViewModel searchViewModel);

    @Binds
    @IntoMap
    @ViewModelKey(RepoViewModel.class)
    abstract ViewModel bindRepoViewModel(RepoViewModel repoViewModel);

    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(GithubViewModelFactory factory);
}

引用元 : https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelModule.java

同様にFragmentの定義、Activityの定義を以下のように書きます。

FragmentBuildersModule.java
@Module
public abstract class FragmentBuildersModule {
    @ContributesAndroidInjector
    abstract RepoFragment contributeRepoFragment();

    @ContributesAndroidInjector
    abstract UserFragment contributeUserFragment();

    @ContributesAndroidInjector
    abstract SearchFragment contributeSearchFragment();
}

引用元 : https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/FragmentBuildersModule.java

MainActivityModule.java
@Module
public abstract class MainActivityModule {
    @ContributesAndroidInjector(modules = FragmentBuildersModule.class)
    abstract MainActivity contributeMainActivity();
}

引用元 : https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/MainActivityModule.java

FragmentBuildersModuleにはアプリ内に存在するinjectが必要なすべてのFragmentについて記述することになります。
Activityに関してはActivityごとにModuleを分けて書いていますが、それらのModuleへの参照をAppComponentに書くことになります。

AppComponents.java
@Singleton
@Component(modules = {
        AndroidInjectionModule.class,
        AppModule.class,
        MainActivityModule.class
})
public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance Builder application(Application application);
        AppComponent build();
    }
    void inject(GithubApp githubApp);
}

引用元 : https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java

全く問題のない方法ですが、Activityを追加する際には以下の3箇所に分けて記述を追加する必要があります。

  • Activityの定義は各ActivityごとのModuleとAppComponent
  • Fragmentの定義はFragmentBuildersModule
  • ViewModelの定義はViewModelModule

このように記述を追加しなければいけない箇所が分かれているのは、場合によってはつらいかもしれません。
また、3つともがアプリ全体の構造を知っているクラスということになります。

ViewModelProvider.FactoryをSubComponentごとに生成する場合

SubComponentを使うことで、上記のうち、FragmentとViewModelの定義を1箇所にまとめることができます。
ActivityごとにModuleを作るのは同様ですが、その配下にFragmentやViewModelに関しての定義も入れる、という実装です。

ただ、そのModuleで定義されたSubComponentごとにViewModelProvider.Factoryが生成されることになるので、Factoryのインスタンスをシングルトンにすることはできません。
SubComponetを使う場合は以下のように@ContributesAndroidInjectorアノテーションの引数にサブコンポーネント用のModuleを指定します。

UiModule.kt
@Module
internal abstract class UiModule {

  @Binds
  abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

  @ContributesAndroidInjector(modules = arrayOf(MainModule::class))
  internal abstract fun contributeMainActivity(): MainActivity

}

MainModuleにはこのMainActivityに所属するFragmentやViewModelの定義を書きます。

MainModule.kt
@Module
internal abstract class MainModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel::class)
  abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel

  @ContributesAndroidInjector
  abstract fun contributeMainFragment(): MainFragment

}

このようにすることで、Dagger2によって生成されるコードは以下のようになり、SubComponentが使われていることがわかると思います。

DaggerAppComponent.java

private final class MainActivitySubcomponentImpl
      implements UiModule_ContributeMainActivity$app_debug.MainActivitySubcomponent {
    private GitHubRepository_Factory gitHubRepositoryProvider;
    private MainViewModel_Factory mainViewModelProvider;

    private Provider<MainModule_ContributeMainFragment.MainFragmentSubcomponent.Builder>
        mainFragmentSubcomponentBuilderProvider;

    private MainActivitySubcomponentImpl(MainActivitySubcomponentBuilder builder) {
      initialize(builder);
    }

    private Map<Class<? extends ViewModel>, Provider<ViewModel>>
        getMapOfClassOfAndProviderOfViewModel() {
      return Collections.<Class<? extends ViewModel>, Provider<ViewModel>>singletonMap(
          MainViewModel.class, (Provider) mainViewModelProvider);
    }

    private ViewModelFactory getViewModelFactory() {
      return new ViewModelFactory(getMapOfClassOfAndProviderOfViewModel());
    }

    //省略
}

このコードのgetViewModelFactory()というメソッド内を見ればわかるように、このSubComponent内ではViewModelFactoryのインスタンスが都度生成されています。
そして、ActivityやFragment内でMainViewModelのインスタンスを取得する処理は以下のようになります。

MainActivity.kt
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
MainFragment.kt
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MainViewModel::class.java)

この時、Dagger2によってinjectされるviewModelFactoryのインスタンスはMainActivity内と、MainFragment内とで、別々のインスタンスとなります。
しかし、MainFragment内でもViewModelProviders::ofの引数にActivityを指定しているので、MainViewModelのインスタンスはMainActivity内でもMainFragment内でも同一のインスタンスが取得されることになります。
これはすでに説明したように、取得できるFragmentManagerが同一であり、先に呼ばれたViewModelProviders:ofの内部でMainViewModelのインスタンスが生成され、次回以降もそのインスタンスが利用されるためです。
したがって、引数として与えたViewModelProvider.Factory型のクラスが実際に使われるのは先に呼ばれた1回のみなので、Activity内とFragment内のそれぞれでのインスタンスが異なっていてもMainViewModelクラスのインスタンスは同一、ということになります。

Dagger2においてはSubComponentを利用する際に、独自のScopeを指定することで、そのインスタンスを同一に保つことができますが、ViewModelに関してはそのScopeも不要ということになります。
Fluxアーキテクチャを利用した例においては、MainDispatcherクラスのインスタンスをSubComponent内で同一にするために、以下のようにPerActivityScopeというScopeを用いています。

MainModule.kt
@Module
internal abstract class MainModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainStore::class)
  abstract fun bindMainStore(viewModel: MainStore): ViewModel

  @ContributesAndroidInjector
  abstract fun contributeRepoDetailDialogFragment(): RepoDetailDialogFragment
}

@Module
internal class MainDispatcherModule {

  @PerActivityScope
  @Provides
  fun provideMainDispatcher() = MainDispatcher()

}

引用元 : https://github.com/satorufujiwara/android-flux-architecture/blob/master/flux-arch/app/src/main/java/jp/satorufujiwara/flux/di/modules/MainModule.kt

このサンプルの詳細については以下の記事に詳しく書いています。

まとめると、SubComponentを利用した場合は、

  • ActivityごとにSubComponentを作り、MainModuleのようにその画面に属する依存性のための処理をまとめることができる
  • すべてのActivityの種類を知っているのは、UiModuleのような1つのクラスだけになる
  • ただし、都度ViewModelProvider.Factory型のインスタンスが生成されることになる

となり、好みの問題かもしれませんが「アプリ全体の構造を知っているクラス」を1つに絞ることができます。

Dagger2によってViewModelをInjectする

すでに説明したようにViewModelクラスはViewModelProviders:of経由で取得する必要があります。
Dagger2と併用した場合に、このViewModelクラスだけ@Injectアノテーションがついてないことで、間違いが起きる可能性があります。
ViewModel型のクラスについてもDagger2を用いてinjectする方法を同僚のstsnさんに教えていただきました。

MainModule.kt
@Module
abstract class ActivityProviderModule<in T : FragmentActivity, VM : ViewModel>(
    private val kclass: Class<VM>
) {
  @Provides
  fun providedActivity(activity: T): FragmentActivity = activity

  @Provides
  fun provideClass(): Class<VM> = kclass

  @Provides
  fun provideViewModel(
      activity: FragmentActivity,
      kclass: Class<VM>,
      factory: ViewModelInjectorFactory<VM>
  ): VM {
    return ViewModelProviders.of(activity, factory).get(kclass)
  }
}

引用元 : https://github.com/satorufujiwara/android-flux-architecture/blob/master/flux-arch/app/src/main/java/jp/satorufujiwara/flux/di/modules/MainModule.kt

上記のように、ViewModelsProviders::ofViewModelProvider::getに必要な引数であるFragmentActivityClass<ViewModel>についてもDagger2を用いてinjectすることによってViewModel@Injectというアノテーションをつけて、Dagger2によるinjectが可能になります。

まとめ

  • Architecture Components を Dagger2 と併用する際はMultibinding機能を用いた独自のViewModelProvider.Factoryを使う
  • SubComponetを用いてみたり、ViewModelもDagger2を用いてinjectするなど、まだまだ改善の余地がありそう
115
83
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
115
83