Edited at

Android Architecture BlueprintsでDagger2のAndroid拡張を理解する


はじめに

Android Architecture Blueprintsのtodo-mvp-daggerで使用されているDagger2のAndroid拡張がよくわからないという方向けに、Android拡張がどのように動作するか解説する記事です。Dagger2についてある程度知っていることが前提となります。


前提


  • Dagger 2.16


概要

Android拡張を使わない場合、ActivityやFragmentにMember Injectionするためには、ライフサイクルメソッド(それぞれ、onCreate, onAttach)で以下のような定型的なコードを書く必要があります。(以下のコードはDagger2 Users Guide/Androidから引用したものです。)


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((SomeApplicationBaseType) getContext().getApplicationContext())
.getApplicationComponent()
.newActivityComponentBuilder()
.activity(this)
.build()
.inject(this);
}

しかし、Android拡張を使って以下の手順を行うと、上記の定型的なコードを書く必要がなくなります。



  1. ApplicationComponent@Component.modulesAndroidSupportInjectionModuleを追加する。


  2. ActivityBindingModuleを作成し、ApplicationComponent@Component.modulesに設定する。


  3. ActivityBindingModuleでは、Member Injectionの対象となるActivityを戻り値とした引数なしメソッドを宣言する。そのメソッドには、@ContributeAndroidInjectorを付与する。これにより、ActivityにMember InjectionするためのSubComponent(=AndroidInjector<対象のActivity>)とAndroidInjector<対象のActivity>.Factoryが自動生成される。このAndroidInjector<対象のActivity>.Factoryは1.のAndroidInjectionModuleMap<Class, AndroidInjector.Factory>にActivityのクラス名をキーとしてMultiBindingされる。

  4. ActivityでDaggerAppCompatActivityを継承する。

これにより、Activityは以下のように振る舞います。


  1. ActivityのonCreateAndroidInjection.inject(this)が実行される。

  2. AndroidInjection.inject(this)で、Applicationが保持しているDispatchingAndroidInjectorinject(this)が実行される。


  3. DispatchingAndroidInjector.injectでは、AndroidInjectionModuleによりDispatchingAndroidInjectorにInjectされたMap<Class, AndroidInjector.Factory>から、ActivityのAndroidInjector.Factoryを取得する。

  4. 取得したAndroidInjector.FactoryからActivityのAndroidInjectorを生成し、AndroidInjector.injectを呼び出し、ActivityにMember Injectionする。

結果として、定型的なコードは不要となり、Activityは「Injectorは何であるか」、「InjectorがどのようにInjectするか」を知る必要がなくなります。


詳細

ここからは、Android Architecture Blueprintsのコードを確認しながら、解説していきます。(Android Architecture Blueprintsgit cloneし、todo-mvp-daggerのbranchに切り替えるとtodo-mvp-daggerのコードを確認できます。)

Task一覧画面(tasksパッケージ)に関わる赤枠の部分を中心に見ていきます。

スクリーンショット 2019-06-23 9.36.07.png

Task一覧画面

スクリーンショット 2019-06-23 9.39.13.png


AppComponent@Component.modulesAndroidSupportInjectionModuleを追加する。

AppComponent@Component.modulesAndroidSupportInjectionModuleを追加します。


AppComponent.java

package com.example.android.architecture.blueprints.todoapp.di;

// 省略
import dagger.android.support.AndroidSupportInjectionModule;

@Singleton
@Component(modules = {/* 省略 */
AndroidSupportInjectionModule.class})
public interface AppComponent extends AndroidInjector<ToDoApplication> {
// 省略
}


AndroidSupportInjectionModuleの中身を確認すると次のようになっています。

package dagger.android.support;

import android.support.v4.app.Fragment;
import dagger.android.AndroidInjectionModule;
// 省略

@Beta
@Module(includes = AndroidInjectionModule.class)
public abstract class AndroidSupportInjectionModule {
@Multibinds
abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
supportFragmentInjectorFactories();
// 省略
}

@Module.includesしているAndroidInjectionModuleは次のようになっています。

package dagger.android;

// 省略
@Beta
@Module
public abstract class AndroidInjectionModule {
@Multibinds
abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
activityInjectorFactories();

@Multibinds
abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
fragmentInjectorFactories();
// 省略
}

AndroidInjectionSupportModuleAndroidInjectionModuleを見ると、support.Fragmentapp.Fragmentapp.Activityのそれぞれについて、Map<Class, AndroidInjector.Factory>@Multibindsの宣言をしています。

後述しますが、この宣言により、TodoApplicationが保持するDispatchingAndroidInjectorMap<Class, AndroidInjector.Factory>型フィールドに、後述のActivityBindingModule@ContributeAndroidInjectorにより生成したAndroidInjector.Factory<TasksActivity>がInjectされます。そして、Activity.onCreateで実行されるDispatchingAndroidInjector.injectで、injectされたMap<Class, AndroidInjector.Factory>からTasksActivityに対応するAndroidInjector.Factoryを取得し、AndroidInjectorを生成し、AndroidInjector.injectTasksActivityに依存をInjectします。


ActivityBindingModuleを作成し、ApplicationComponent@Component.modulesに設定する。

ActivityBindingModuleを作成します(名前は何でも良いです)。今は中身は空です。

次の節で、ActivityにMember Injectionするための処理を記述します。


ActivityBindingModule.java

package com.example.android.architecture.blueprints.todoapp.di;

// 省略
@Module
public abstract class ActivityBindingModule {
// 省略(中身は後述します。)
}


ApplicationComponent@Component.modulesに設定します。


AppComponent.java

package com.example.android.architecture.blueprints.todoapp.di;

// 省略
import dagger.android.support.AndroidSupportInjectionModule;

@Singleton
@Component(modules = {/* 省略 */
ActivityBindingModule.class,
AndroidSupportInjectionModule.class})
public interface AppComponent extends AndroidInjector<ToDoApplication> {
// 省略
}



ActivityBindingModuleで、Member Injectionの対象となるActivityを戻り値とした引数なしメソッドを宣言する。メソッドには@ContributeAndroidInjectorを付与する。

ActivityBindingModuleTasksActivityを戻り値としたメソッドを宣言し、@ContributeAndroidInjectorをつけます。


ActivityBindingModule.java

package com.example.android.architecture.blueprints.todoapp.di;

// 省略
@Module
public abstract class ActivityBindingModule {
@ActivityScoped
@ContributesAndroidInjector(modules = TasksModule.class)
abstract TasksActivity tasksActivity();
// 省略
}


これにより、以下のコードが自動生成されます。



  • ActivityBindingModule_TasksActivity.TasksActivitySubcomponent: TasksActivityに依存をInjectするためのSubComponent(=AndroidInjector<TasksActivity>)です。


  • ActivityBindingModule_TasksActivity: TasksActivitySubComponent.Builderを、AndroidSupportInjectionModuleで宣言しているMap<Class, AndroidInjector.Factory>にMultiBindingするModuleです。(BuilderはAndroidInjector.Factoryのサブインターフェイスです。)


【自動生成】ActivityBindingModule_TasksActivity.java

package com.example.android.architecture.blueprints.todoapp.di;

// 省略
@Module(subcomponents = ActivityBindingModule_TasksActivity.TasksActivitySubcomponent.class)
public abstract class ActivityBindingModule_TasksActivity {
private ActivityBindingModule_TasksActivity() {}

@Binds
@IntoMap
@ActivityKey(TasksActivity.class)
abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
TasksActivitySubcomponent.Builder builder);

@Subcomponent(modules = TasksModule.class)
@ActivityScoped
public interface TasksActivitySubcomponent extends AndroidInjector<TasksActivity> {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<TasksActivity> {}
}
}


これらの自動生成されたコードにより、AndroidInjectionModule@MultiBindsで宣言しているMap<Class, AndroidInjector.Factory>に、TasksActivity.classをキーとしてAndroidInjector.Builder<TasksActivity>(BuilderはAndroidInjector.Factoryのサブインターフェイス)がBindsされます。

補足として、上記のActivityBindingModule.tasksActivity()@ActivityScopedは生成されるSubComponentのScopeになります。また、modules=TasksModule.javaはSubComponentにinstallするModuleです。

TasksModuleでは、ActivityBindingModuleでTasksActivityについて行ったのと同様に、TasksFragmentを戻り値とする引数なしメソッドを宣言し、@ContributeAndroidInjectorを付与します。

こうすることで、TasksActivityの時と同様に、AndroidInjectionModule@MultiBindsで宣言しているMap<Class, AndroidInjector.Factory>に、TasksFragment.classをキーとしてAndroidInjector.Builder<TasksFragment>がBindsされます。


TasksModule.java

package com.example.android.architecture.blueprints.todoapp.tasks;

// 省略
@Module
public abstract class TasksModule {
@FragmentScoped
@ContributesAndroidInjector
abstract TasksFragment tasksFragment();

@ActivityScoped
@Binds abstract TasksContract.Presenter taskPresenter(TasksPresenter presenter);
}



ActivityでDaggerAppCompatActivityを継承する。

ActivityでDaggerAppCompatActivityを継承します。


TAsksActivity.java


package com.example.android.architecture.blueprints.todoapp.tasks;
// 省略
public class TasksActivity extends DaggerAppCompatActivity { /*省略*/ }

DaggerAppCompatActivityを継承することにより、次の通り、onCreateAndroidInjection.inject(this)が実行されるようになります。


DaggerAppCompatActivity.java

package dagger.android.support;

// 省略
@Beta
public abstract class DaggerAppCompatActivity extends AppCompatActivity
implements HasFragmentInjector, HasSupportFragmentInjector {
// 省略
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
}
// 省略
}

AndroidInjection.inject(this)を実行すると、TodoApplication(DaggerApplicationを継承)を取得します。そして、TodoApplicationで保持している、activityInjector(DispatchingAndroidInjector<Activity>型)を取得します。そして、activityInjectorinject(activity)が実行されます。


DaggerApplication.java

package dagger.android;

// 省略
@Beta
public abstract class DaggerApplication extends Application
implements HasActivityInjector /* 省略 */ {

@Inject DispatchingAndroidInjector<Activity> activityInjector;
}


activityInjector(DispatchingAndroidInjector<Activity>型)のinject(activity)では、AndroidInjectionModule@Multibindsの宣言により、InjectされたinjectorFactoriesから、TasksActivityに対応する、AndroidInjector.Factoryを取得し、AndroidInjectorを生成し、TasksActivityに依存をInjectします。


DispatchingAndroidInjector.java

package dagger.android;

@Beta
public final class DispatchingAndroidInjector<T> implements AndroidInjector<T> {
// 省略
private final Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>>
injectorFactories;

@Inject
DispatchingAndroidInjector(
Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>> injectorFactories) {
this.injectorFactories = injectorFactories;
}
// 省略


これで、TasksActivityに依存がInjectされるまでの流れは終了です。


最後に

この記事ではAndroid拡張がどのような仕組みになっているかをざっくり解説しました。最初にAndroid拡張のコードを見たときは、何をしているかわからなかったのですが、自動生成されたコードも含めて一つ一つ見ていくと、Android拡張はDaggerの基本的な仕組み(Multibinding, SubComponent)を使って実現されていることがわかりました。