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の悩みポイントを挙げておく。

これらの特徴から、一筋縄では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の機能によって、MainActivity1SubComponentAppComponent の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インスタンスを用いて、 MainActivity1ViewModelFactory がinjectされる。そして、この ViewModelFactory インスタンスを用いて、 MainActivity1ViewModel1 インスタンスが生成される。
ViewModelFactory 内で ViewModelFactory#create が呼ばれると、 injectされたMapから対象のViewModelのファクトリクラスのインスタンスを介してViewModelのインスタンス化を行う。この際に、ViewModelへの依存性の充足も行われるのである。

DaggerのMultiBindings機能を用いることによって、あらかじめViewModelのファクトリクラスが用意されるため、Activity側からComponentを利用したinjectを行うことなく、ViewModel内の依存を充足させることができる。