Daggerがやってくれること (AAC ViewModel編)
前回、Daggerがやってくれること (Android Support編)を書いた。
今回は、これにAndroid Architecture ComponentのViewModelを適用した際に、ViewModelに対してどのようにdiが実現されるのかを見ていく。
TL;DR
- DaggerのMultiBindings機能を用いることで、ViewModelクラスをkeyとしたViewModelファクトリクラスのインスタンスのMapが用意される
ViewModelProvider#Factory
のサブクラスにこのMapを注入することで、ViewModelProvider#Factory#create
内でViewModelのインスタンス化と、依存の充足を行う
ViewModelとDaggerを用いたdiの実装方法に関しては詳しく書かない。今回は、 github.com を参考に実装する。
実装の説明に入る前に、ViewModelへのinjectの悩みポイントを挙げておく。
- ViewModelProvidersを使用して、インスタンスが作られる
- ViewModelからApplicationを参照できない (つまり、Componentを参照できない)
- AndroidViewModelを使用してApplicationを参照できるが、メモリリークの恐れとdiのためだけにApplicationを参照したいことを考えると使用したくない
これらの特徴から、一筋縄ではdiを実現することができない。
しかし、DaggerのMultiBindings機能を使用することで、ViewModelに対してもdiを実現することが可能である。
それでは、実装を見ていく。
class MainViewModel1 @Inject constructor( private val messageApi: MessageApi ) : ViewModel() { private val mutableMessage: MutableLiveData<String> = MutableLiveData() val message: LiveData<String> = mutableMessage fun onButtonClicked() { val message = messageApi.getMessage() mutableMessage.value = message } }
まず、ViewModelを作成する。
ViewModel1
では、 ViewModel1#onButtonClicked
が呼ばれると MessageApi
を介してメッセージを取得し、LiveDataの値を更新する。
MessageApi
のインスタンスはコンストラクタインジェクションによって、injectされる。
class MainActivity1 : AppCompatActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory private val viewModel: MainViewModel1 by lazy { ViewModelProviders.of(this, viewModelFactory).get(MainViewModel1::class.java) } private lateinit var messageTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) super.onCreate(savedInstanceState) setContentView(R.layout.activity_main_1) messageTextView = findViewById(R.id.message_text_view) viewModel.message.observe(this, Observer { messageTextView.text = it }) findViewById<Button>(R.id.message_button).setOnClickListener { viewModel.onButtonClicked() } findViewById<Button>(R.id.next_button).setOnClickListener { val intent = MainActivity2.createIntent(this@MainActivity1) startActivity(intent) } } }
Activity側では、ViewModelをインスタンス化し、それを介してボタンが押された時の挙動を実装している。
ViewModelFactory
がinjectされ、それを用いて MainViewModel1
がインスタンス化される。
class ViewModelFactory @Inject constructor( private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>> ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { val found = creators.entries.find { modelClass.isAssignableFrom(it.key) } val creator = found?.value ?: throw IllegalArgumentException("unknown model class " + modelClass) try { @Suppress("UNCHECKED_CAST") return creator.get() as T } catch (e: Exception) { throw RuntimeException(e) } } }
ViewModelFactory
には、ViewModelのインスタンス化方法が実装されている。
DaggerのMultiBindings機能を用いて生成される Map<Class<out ViewModel>, Provider<ViewModel>>
のインスタンスをコンストラクタインジェクションによってinjectし、このインスタンスを用いて、ViewModelのインスタンス化を行う。
@Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass<out ViewModel>)
@Module abstract class Main1Module { @Binds @IntoMap @ViewModelKey(MainViewModel1::class) abstract fun bindMainViewModel1(viewModel: MainViewModel1): ViewModel }
Map<Class<out ViewModel>, Provider<ViewModel>>
のインスタンスを生成するために鍵となるのが、 @MapKey
と @IntoMap
である。これらのアノテーションを用いることで、この場合は MainViewModel1
クラスをkeyとした ViewModel1
のファクトリクラスであるProviderのMapが生成される。
@Module abstract class ActivityModule { @ContributesAndroidInjector(modules = [MessageModule::class, Main1Module::class]) @ActivityScope abstract fun contributeMainActivity1Injector(): MainActivity1 }
@Component(modules = [AndroidInjectionModule::class, CreatorModule::class, ActivityModule::class]) @Singleton interface AppComponent : AndroidInjector<App>
MainActivity1
内の依存性を充足させるために、 ActivityModule
内に必要なModuleを追加している。
Dagger Androidの機能によって、MainActivity1SubComponent
が AppComponent
のSubComponentとして定義される。
それでは、Daggerが生成するコードを見ていく。
// DaggerAppComponent private final class MainActivity1SubcomponentBuilder extends ActivityModule_ContributeMainActivity1Injector.MainActivity1Subcomponent.Builder { private MessageModule messageModule; private MainActivity1 seedInstance; @Override public ActivityModule_ContributeMainActivity1Injector.MainActivity1Subcomponent build() { if (messageModule == null) { this.messageModule = new MessageModule(); } Preconditions.checkBuilderRequirement(seedInstance, MainActivity1.class); return new MainActivity1SubcomponentImpl(messageModule, seedInstance); } } private final class MainActivity1SubcomponentImpl implements ActivityModule_ContributeMainActivity1Injector.MainActivity1Subcomponent { private Provider<MessageApi> provideMessageApiProvider; private Provider<MainViewModel1> mainViewModel1Provider; private MainActivity1SubcomponentImpl( MessageModule messageModuleParam, MainActivity1 seedInstance) { initialize(messageModuleParam, seedInstance); } @SuppressWarnings("unchecked") private void initialize( final MessageModule messageModuleParam, final MainActivity1 seedInstance) { this.provideMessageApiProvider = DoubleCheck.provider( MessageModule_ProvideMessageApiFactory.create( messageModuleParam, DaggerAppComponent.this.provideMessageCreatorProvider)); this.mainViewModel1Provider = MainViewModel1_Factory.create(provideMessageApiProvider); } }
まず、 MainActivity1SubcomponentBuilder#build
が呼ばれた時点で、 MessageModule
がインスタンス化され、これを用いて MainActivity1SubcomponentImpl
もインスタンス化される。
そして、 MainActivity1SubcomponentImpl#initialize
が呼ばれ、 MainViewModel1
のファクトリクラスがインスタンス化される。
// DaggerAppComponent private final class MainActivity1SubcomponentImpl implements ActivityModule_ContributeMainActivity1Injector.MainActivity1Subcomponent { private Provider<MessageApi> provideMessageApiProvider; private Provider<MainViewModel1> mainViewModel1Provider; private MainActivity1SubcomponentImpl( MessageModule messageModuleParam, MainActivity1 seedInstance) { initialize(messageModuleParam, seedInstance); } private Map<Class<? extends ViewModel>, Provider<ViewModel>> getMapOfClassOfAndProviderOfViewModel() { return Collections.<Class<? extends ViewModel>, Provider<ViewModel>>singletonMap( MainViewModel1.class, (Provider) mainViewModel1Provider); } private ViewModelFactory getViewModelFactory() { return new ViewModelFactory(getMapOfClassOfAndProviderOfViewModel()); } @SuppressWarnings("unchecked") private void initialize( final MessageModule messageModuleParam, final MainActivity1 seedInstance) { this.provideMessageApiProvider = DoubleCheck.provider( MessageModule_ProvideMessageApiFactory.create( messageModuleParam, DaggerAppComponent.this.provideMessageCreatorProvider)); this.mainViewModel1Provider = MainViewModel1_Factory.create(provideMessageApiProvider); } @Override public void inject(MainActivity1 arg0) { injectMainActivity1(arg0); } private MainActivity1 injectMainActivity1(MainActivity1 instance) { MainActivity1_MembersInjector.injectViewModelFactory(instance, getViewModelFactory()); return instance; } }
そして、 MainActivity1SubcomponentImpl#inject
が呼ばれた時点で、 MainActivity1SubcomponentImpl#getMapOfClassOfAndProviderOfViewModel
を介して、 MainViewmodel1
クラスをkeyとした MainViewModel1
のファクトリクラスのインスタンスのMapが生成される。
この生成されたMapと MainActivity1
のインスタンスを用いて、 MainActivity1
に ViewModelFactory
がinjectされる。そして、この ViewModelFactory
インスタンスを用いて、 MainActivity1
で ViewModel1
インスタンスが生成される。
ViewModelFactory
内で ViewModelFactory#create
が呼ばれると、 injectされたMapから対象のViewModelのファクトリクラスのインスタンスを介してViewModelのインスタンス化を行う。この際に、ViewModelへの依存性の充足も行われるのである。
DaggerのMultiBindings機能を用いることによって、あらかじめViewModelのファクトリクラスが用意されるため、Activity側からComponentを利用したinjectを行うことなく、ViewModel内の依存を充足させることができる。