はじめに
こちらは以下のスライドの前半部分の補足記事となっています。
スライドについても合わせて見ていただければ幸いです。
Kotlin × Architecture Components 実践
https://speakerdeck.com/satorufujiwara/kotlin-x-architecture-components
これまでArchitecture ComponentsとDagger2を組み合わせた実装については、以下のような記事で紹介してきました。
本記事はその中のViewModelProvider.Factoryにフォーカスした記事です。
- Kotlin + Architecture Component + Dagger2によるAndroidアプリ設計
- Architecture Components + Flux (+ Kotlin)によるAndroidアプリ設計
Dagger2の導入やDagger2以外の実装についてはそれぞれの記事をご覧ください。
また、この記事は以下のバージョンのArchitecture Componentsを用いて書かれています。
implementation "android.arch.lifecycle:runtime:1.0.3"
implementation "android.arch.lifecycle:extensions:1.0.0"
kapt "android.arch.lifecycle:compiler:1.0.0"
参考リポジトリ
- https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample
- GoogleによるDagger2を使ったサンプル(Java)、すべての参考元
- https://github.com/googlesamples/android-architecture/tree/todo-mvvm-live
- Dagger2を使わずに
ViewModelProvider.Factoryを自前で作るサンプル - https://github.com/satorufujiwara/kotlin-architecture-components
- Dagger2のSubComponentを使ったサンプル
- https://github.com/satorufujiwara/android-flux-architecture/tree/master/flux-arch
- Dagger2のSubComponentを使った上、ViewModel以外のクラスを
PerActivityScopeを使ってInjectするサンプル - https://github.com/satoshun-example/DaggerArchitectureComponent
-
ViewModelクラスもDagger2を用いてinjectするサンプル
Architecture ComponentsとDagger2を組み合わせる理由
まずはArchitecture ComponentsとDagger2を併用する理由と、その際に理解しておきたい部分について解説します。
ViewModelのインスタンスはViewModelProvider経由で取得する
class MainViewModel : ViewModel()
上記のようなViewModelを継承したMainViewModelというクラスがある場合、このクラスのインスタンスを取得する際は以下のようにします。
val viewModelProvider: ViewModelProvider = ViewModelProviders.of(this)
//ViewModelを継承したクラスを指定する
val viewModel: MainViewModel = viewModelProvider.get(MainViewModel::class.java)
この際、**ViewModelProviders::ofから取得したViewModelProviderクラスの関数getからインスタンスを取得する**必要があります。
上記はActivity内の例でしたが、Fragment内で取得する場合は以下のような挙動になります。
viewModelA = ViewModelProviders.of(activity!!).get(MainViewModel::class.java)
// viewModelAとは別のインスタンス
viewModelB = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModelAとviewModelBは同じ型のクラスですが別のインスタンスです。
ViewModelProviders::ofに指定できる引数はFragmentActivityもしくはFragmentですが、それぞれの場合において内部的には以下のような処理が呼ばれています。
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の中はさらに以下のようになっていて、
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関数です。
public static HolderFragment holderFragmentFor(FragmentActivity activi
return sHolderFragmentManager.holderFragmentFor(activity);
}
public static HolderFragment holderFragmentFor(Fragment fragment) {
return sHolderFragmentManager.holderFragmentFor(fragment);
}
さらに辿ることでHolderFragment.HolderFragmentManagerクラスに行き着きます。
このクラス内のholderFragmentFor関数がそれぞれ呼ばれています。
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のインスタンスが同一がどうかが決まります**。
HolderFragmentはViewModelStore型のメンバ変数mViewModelStoreを持っていて、ViewModelStoreはHashMap<String, ViewModel>型のメンバ変数を用いてViewModelのインスタンスを管理しています。
このHashMap型の変数のString型のキーは以下のように決められています。
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つしか指定しませんでしたが、この場合、内部的には以下のようになっています。
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
initializeFactoryIfNeeded(checkApplication(activity));
return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}
sDefaultFactoryというのはViewModelProvider.Factoryを継承したクラスで、以下のような実装になっています。
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は以下のようなクラスです。
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);
}
}
}
それぞれの実装において注目したいのは、DefaultFactoryのreturn modelClass.getConstructor(Application.class).newInstance(mApplication);の部分と、ViewModelProvider.NewInstanceFactoryのreturn modelClass.newInstance();の部分です。
その実装に注目すると、sDefaultFactoryを使った場合、
-
AndroidViewModelを継承したViewModelの場合、コンストラクタにApplicationクラスが引数として渡され、インスタンスが生成される - 上記ではない
ViewModelの場合、引数なしのコンストラクタが呼ばれ、インスタンスが生成される
という挙動になります。
したがって、次のViewModelクラスのように、Applicationクラス以外をコンストラクタの引数としたい場合はDefaultFactoryではない、独自のViewModelProvider.Factoryが必要になります。
class MainViewModel constructor(private val repository: GitHubRepository) : ViewModel() {
上記のMainViewModelクラスのインスタンスを生成できるように、独自のViewModelProvider.Factoryを作ると以下のようになります。
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());
}
}
メソッドpublic <T extends ViewModel> T create(Class<T> modelClass)内の実装に注目してください。
このメソッド内で、if文による場合分けが必要になります。
また、メソッドgetInstanceで行っているように、ViewModelFactory自身をシングルトンにする工夫なども必要になります。
Applicationクラス以外をコンストラクタの引数としたい場合、このような手間を省くためにDagger2を使うことができます。
Dagger2のMultibinding機能を使ってViewModelのインスタンスを生成する
Dagger2を利用して、独自のViewModelProvider.Factoryを使う場合はMultibinding機能を使います。
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
上記のようにキーを指定するためのアノテーションを定義して、Module内には以下のように記述します。
@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は以下のように定義します。
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]の箇所で、modelClass``MainViewModel::classが指定されれば、creatorはMainViewModel型を返すProviderということになります。実際に、return creator.get() as Tとして、creatorからMainViewModel型のインスタンスを取得し、キャストした上で返しています。
ここでのProviderはDagger2内のインタフェースで、Provider injectionの機能を利用するためのものです。
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によって生成されるコードは以下のようになっていて、
//いろいろ省略
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());
}
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のコンストラクタに代入されるcreatorはMap<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を取得してから、creatorがnullなら再度マップ内を探索しています。
手元ではcreatorがnullとならなかったので、後続の処理が必要な理由がいまいち不明です。
参考となるリポジトリでも、最初のcommitからそのようになっていました。
こちらに関してもしご存知の方がいたらTwitterやコメントなどで教えてください。
SubComponetを使うかどうか
GitHubBrowserSampleのGitHubViewModelFactoryには@Singletonというアノテーションがついていますが、kotlin-architecture-componentsのViewModelFactoryにはついていません。
前者は文字通りアプリ内で唯一のインスタンスを持ちますが、後者はインスタンスが必要な時に都度生成をしています。
この違いはSubComponentを使うかどうかの違いによるものです。
ViewModelProvider.Factoryをシングルトンにする場合
まずは@Singletonをつけた場合についてどうなるか考えます。この場合、そのコンストラクタにinejectされるMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>型の引数creatorsもシングルトンになります。
したがってその元となるModuleの定義も以下のようにまとめて書く必要があります。
@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);
}
同様にFragmentの定義、Activityの定義を以下のように書きます。
@Module
public abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract RepoFragment contributeRepoFragment();
@ContributesAndroidInjector
abstract UserFragment contributeUserFragment();
@ContributesAndroidInjector
abstract SearchFragment contributeSearchFragment();
}
@Module
public abstract class MainActivityModule {
@ContributesAndroidInjector(modules = FragmentBuildersModule.class)
abstract MainActivity contributeMainActivity();
}
FragmentBuildersModuleにはアプリ内に存在するinjectが必要なすべてのFragmentについて記述することになります。
Activityに関してはActivityごとにModuleを分けて書いていますが、それらのModuleへの参照をAppComponentに書くことになります。
@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);
}
全く問題のない方法ですが、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を指定します。
@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の定義を書きます。
@Module
internal abstract class MainModule {
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
@ContributesAndroidInjector
abstract fun contributeMainFragment(): MainFragment
}
このようにすることで、Dagger2によって生成されるコードは以下のようになり、SubComponentが使われていることがわかると思います。
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のインスタンスを取得する処理は以下のようになります。
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
@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を用いています。
@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()
}
このサンプルの詳細については以下の記事に詳しく書いています。
まとめると、SubComponentを利用した場合は、
- ActivityごとにSubComponentを作り、
MainModuleのようにその画面に属する依存性のための処理をまとめることができる - すべてのActivityの種類を知っているのは、
UiModuleのような1つのクラスだけになる - ただし、都度
ViewModelProvider.Factory型のインスタンスが生成されることになる
となり、好みの問題かもしれませんが「アプリ全体の構造を知っているクラス」を1つに絞ることができます。
Dagger2によってViewModelをInjectする
すでに説明したようにViewModelクラスはViewModelProviders:of経由で取得する必要があります。
Dagger2と併用した場合に、このViewModelクラスだけ@Injectアノテーションがついてないことで、間違いが起きる可能性があります。
ViewModel型のクラスについてもDagger2を用いてinjectする方法を同僚のstsnさんに教えていただきました。
AACのViewModel使うとDaggerのフローから外れるの微妙。Factory配るパターンがメジャーなようにみえるけど、それだとViewModelを直接 [at]Injectしたときにおかしくなるから、間違った使いかたが出来るので良くない
— Sato Shun△ (@stsn_jp) 2018年1月15日
@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)
}
}
上記のように、ViewModelsProviders::ofやViewModelProvider::getに必要な引数であるFragmentActivity、Class<ViewModel>についてもDagger2を用いてinjectすることによってViewModelも@Injectというアノテーションをつけて、Dagger2によるinjectが可能になります。
まとめ
- Architecture Components を Dagger2 と併用する際はMultibinding機能を用いた独自の
ViewModelProvider.Factoryを使う - SubComponetを用いてみたり、
ViewModelもDagger2を用いてinjectするなど、まだまだ改善の余地がありそう