はじめに
こちらは以下のスライドの前半部分の補足記事となっています。
スライドについても合わせて見ていただければ幸いです。
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
の引数(FragmentActivity
orFragment
)から取得できる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するなど、まだまだ改善の余地がありそう