Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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.のAndroidSupportInjectionModule(厳密にはそれが保持している AndroidInjectionModule)のMap<Class, AndroidInjector.Factory>にActivityのクラス名をキーとしてMultiBindingされる。
  4. ActivityでDaggerAppCompatActivityを継承する。

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

  1. ActivityのonCreateでベースクラスDaggerAppCompatActivityAndroidInjection.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)を使って実現されていることがわかりました。

yuki_m
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away