184
128

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

とてもつもなくわかりやすいdagger2(2.11)入門

Last updated at Posted at 2018-02-14

とてもつもなくわかりやすいdagger2(2.11)入門

注意

この記事の内容は3年ほど前に書きました。筆者はもうAndroidから離れており、内容がかなり古くなっています。ご注意ください。

依存注入の考え方

ハト君はandroidプログラマーとして1年と少し。だいぶjavaに慣れてたくさんのプロジェクトを作成していた。しかし、最近彼には悩みがある。テストの重要性はわかってはいるのだが、どうすればテストしやすいプログラムが書けるのかわからない。AndroidSudioでユニットテストするにはとりあえず、MainActivityにすべてを書かず、クラスとして書き出せばいけることは最近気づいた。ただ、例えばこういう他のクラスに依存しているクラスはとてもテストしづらいのだ。

public class CoffeeMaker{

  private Heater heater;
  private Pump pump;

  CoffeeMaker(){
    heater = new Heater();
    pump = new Pump(heater);
  }
  
  public void drip(){
   heater.heating();
    pump.pumping();
    System.out.println("Complete!")
  }
}
public class Pump{
  final private Heater heater;

  Pump(Heater heater){
    this.heater = heater;
  }
  
  public void pumping(){
    if(heater.isHot()){
      System.out.println("pumping");
    }
    
  }
}
public class Heater{

  private Boolean isHot = false;

  Heater(){}
  
  public void heating(){
    isHot = true;
    System.out.println("heating")
  }
  
  public Boolean isHot(){return isHot;}
}

CoffeeMakerクラスはHeaterクラスとPumpクラスに依存しているため、CoffeeMakerクラス単独でテストができない。また、PumpクラスやHeaterクラスをダミークラスに変えることもできない。そこで次のことを考えついた。

public class CoffeeMaker{

  private Heater heater;
  private Pump pump;

  CoffeeMaker(Heater heater, Pump pump){
    this.heater = heater;
    this.pump = pump;
  }
  
  public void drip(){
    heater.heating();
    pump.pumping();
    System.out.println("Complete!")
  }
}

こうすれば、CofeeMakerクラスを作る時、コンストラクタにPumpを Heaterを渡すことができ、テストしやすくなった。大変満足。友人のモズ君に自慢しに行ったところ、依存注入(DpendencyInjection)というすでにある考え方らしい。残念。

3日後、大変なことになった。調子に乗ってさまざまなCofeeMakerクラスを作っていたんだが...

  CoffeeMaker cm1 = new CoffeeMaker(new Heater(), new Pump())
  CoffeeMaker cm2 = new CoffeeMaker(new Heater(), new Pump())
  CoffeeMaker cm3 = new CoffeeMaker(new Heater(), new Pump())
  CoffeeMaker cm4 = new CoffeeMaker(new Heater(), new Pump())
  CoffeeMaker cm5 = new CoffeeMaker(new Heater(), new Pump())
  // これが後10個くらいいろんなところに散らばってる。

しかし、ここでどうしてもHeaterの代わりにSuperHeaterを使いたくなった。

public class SuperHeater{

  SuperHeater(){}
  
  public void heating(){
    System.out.println("super_heating")
  }
}

たくさんCoffeeMakerクラスを作ったので手作業で書き換えるのは大変だし、AndroidStudioの置換機能はほかの部分に影響を与えるのが怖いためなるべく使いたくない。今回は仕方ないが、今後このようなことがないために実装方法を変えることにした。

  CoffeeMaker cm1 = new CoffeeMaker(HeaterBuilder.build(), PumpBuilder.build())
  CoffeeMaker cm2 = new CoffeeMaker(HeaterBuilder.build(),PumpBuilder.build())
  CoffeeMaker cm3 = new CoffeeMaker(HeaterBuilder.build(), PumpBuilder.build())
  CoffeeMaker cm4 = new CoffeeMaker(HeaterBuilder.build(),PumpBuilder.build())
  CoffeeMaker cm5 = new CoffeeMaker(HeaterBuilder.build(),PumpBuilder.build())
  // これが後10個くらいいろんなところに散らばってる。
public class HeaterBuilder {
  
  public static Heater build(){
    // return new Heater();
    return new SuperHeater();
  }
}
public class PumpBuilder {
  
  public static Pump build(){
    return new Pump();
  }
}

こうすれば、Builderの中身を変えるだけで、全てのコンストラクタを変えることができるし、テストもしやすい。やったねハト君。ただし、HeaterBuilder.build()をこれからことあるごとに作っていくはしんどいし、Activityといったライフサイクルを持つやつもあって、すべてうまくいくとは思えないな。

モズ君曰く、こういうことうまくやってくれる魔法のようなライブラリがあるらしい。それはDagger2というらしい。魔法といいダガーといいモズ君はふぁんたじーにでも目覚めたのだろうか。

Dagger2の導入の仕方

Dagger2実際に存在した。しかもあのGoogleさんがメンテナンスしてるらしい。強い。さっそくAndroidStudioにDagger2を導入してみる。AppModuleの方のbiuld.gradleのdependenciesに以下を追加する。

dependencies{
implementation "com.google.dagger:dagger:2.11"
implementation "com.google.dagger:dagger-android-support:2.11"
annotationProcessor "com.google.dagger:dagger-android-processor:2.11"
annotationProcessor "com.google.dagger:dagger-compiler:2.11"
}

あとは、Syncして出来上がり。

Dagger2の基本の使い方

導入はできたものの困ったことになった。Githubのサンプルたちをみても、みなさん実装の仕方が少しずつ違うし、ネットの情報はバージョンが古いやつばっか。とりあえず、基本の部分だけ今回は抑えようと思う。

Injectといったアノテーション

Dagger2はアノテーションを多用する。Dagger2にでてくるアノテーションは主にこんな感じのやつら

アノテーション 説明
@Inject これつけてるとDaggerが勝手にnewして代入(注入)してくれる。
@Module 後述するModuleにつける。
@Component 後述するComponentにつける

まだまだあるが、Dagger2は基本的にアノテーションで記述していくことをまず、押さえておいてほしい。

基本のきInject

一番最初のCoffeeMakerクラスDagger2で書き換えると次のようになる。

public class CoffeeMaker{

  @Inject Heater heater;
  @Inject Pump pump;

  @Inject
  CoffeeMaker(){
  }
  
  public void drip(){
    heater.heating();
    pump.pumping();
    System.out.println("Complete!");
  }
}

CoffeeMakerクラスのフィールドとPump,Heaterクラスのコンストラクタに@Injectがついたのがわかるだろうか。このようにフィールドに@Injectをつけるだけで、Dagger2が代入(注入)してくれる。

  //Heater heater = new Heater(); 
  //Pump pump = new Pump();
  @Inject Heater heater;
  @Inject Pump pump;

しかし、Dagger2の管理下におくために、クラス(オブジェクト)はコンストラクタに@Injectをつけないといけないので、注意。こうしないとDaggerは注入してくれないらしい。

public class Pump{

  private final Heater heater;

  @Inject
  Pump(Heater heater){
    this.heater = heater;
  }
  
  public void pumping(){
    if(heater.isHot()){
      System.out.println("pumping");
    }
  }
}
public class Heater{

  public Boolean isHot = false;

  @Inject
  Heater(){}
  
  public void heating(){
    isHot = true;
    System.out.println("heating");
  }
  
  public Boolean isHot(){
    return isHot;
  }
}

こんな方法があるのかと目から鱗である。すごい。

ここまでの実装1

ここまでの実装を実際に動かしてみる。@ComponentDaggerMainActivity_Coffeshopはあとで説明する。

public class MainActivity extends AppCompatActivity {

    private CoffeeShop coffeeShop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        coffeeShop = DaggerMainActivity_CoffeeShop.create();
      
       coffeeShop.maker().drip()
    }
    
    @Component
    interface CoffeeShop{
      CoffeeMaker maker();
    }


}
public class CoffeeMaker{

  @Inject Heater heater;
  @Inject Pump pump;

  @Inject
  CoffeeMaker(){
  }
  
  public void drip(){
    heater.heating();
    pump.pumping();
    System.out.println("Complete!");
  }
}
public class Pump{

  private final Heater heater;

  @Inject
  Pump(Heater heater){
    this.heater = heater;
  }
  
  public void pumping(){
    if(heater.isHot()){
      System.out.println("pumping");
    }
  }
}
public class Heater{

  public Boolean isHot = false;

  @Inject
  Heater(){}
  
  public void heating(){
    isHot = true;
    System.out.println("heating");
  }
  
  public Boolean isHot(){
    return isHot;
  }
}

Logcatのぞいて

I/System.out: heating
I/System.out: pumping
I/System.out: Complete!

となっていれば成功。

Component

Dagger2は全ての依存オブジェクトをComponentで管理する。というより、@Componetのついたクラスをもとに、daggerがコードを自動生成し、インジェクトできるようにするのだ。例えば先ほどのMainActivityないで、DaggerMainActivity_CoffeeShopを見たと思うが、これはダガーが次のようなコードを自動生成している。

public final class DaggerMainActivity_CoffeeShop implements MainActivity.CoffeeShop {
  private Provider<Pump> pumpProvider;

  private MembersInjector<CoffeeMaker> coffeeMakerMembersInjector;

  private Provider<CoffeeMaker> coffeeMakerProvider;

  private DaggerMainActivity_CoffeeShop(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  public static MainActivity.CoffeeShop create() {
    return new Builder().build();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {

    this.pumpProvider = Pump_Factory.create(Heater_Factory.create());

    this.coffeeMakerMembersInjector =
        CoffeeMaker_MembersInjector.create(Heater_Factory.create(), pumpProvider);

    this.coffeeMakerProvider = CoffeeMaker_Factory.create(coffeeMakerMembersInjector);
  }

  @Override
  public CoffeeMaker maker() {
    return coffeeMakerProvider.get();
  }

  public static final class Builder {
    private Builder() {}

    public MainActivity.CoffeeShop build() {
      return new DaggerMainActivity_CoffeeShop(this);
    }
  }
}

さいしょの部分に注目すると、

public final class DaggerMainActivity_CoffeeShop implements MainActivity.CoffeeShop{
  ...
  
   @Override
  public CoffeeMaker maker() {
    return coffeeMakerProvider.get();
  }
  
}

@Componentのついたメソッドを@Overrideしている。このメソッドを呼べば、もれなくCoffeeMakerが手に入るというわけだ。

つぎに注意すべきはこのクラスはシングルトンとして提供されていることだ。なぜならコンストラクタをよくみると

private DaggerMainActivity_CoffeeShop(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

privateになっているので、そとからこのクラスをnewできない。このクラスをインスタンス化するには2通りの方法がある。

  DaggerMainActivity_CoffeeShop.create()

もしくは、

  DaggerMainActivity_CoffeeShop
    .builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();

とする。違いはModuleを使うかどうかだ。もしModuleを使わなければcreateメソッドを呼べば良い。中でnew Builder.build()が呼ばれる。Moduleについては次の章で説明する。

Daggerがどのようにinjectしているか

ここの部分は少し詳しすぎるかもしれないのでとりあえず使えるよにしたいという人は飛ばしてもらって構わない。しかし、理解すればdaggerに関してかなり理解が深まるのではないか。

つぎに重要な部分はこのメソッドである。

private void initialize(final Builder builder) {

    this.pumpProvider = Pump_Factory.create(Heater_Factory.create());

    this.coffeeMakerMembersInjector =
        CoffeeMaker_MembersInjector.create(Heater_Factory.create(), pumpProvider);

    this.coffeeMakerProvider = CoffeeMaker_Factory.create(coffeeMakerMembersInjector);
  }

なにやら@Injectの対象である、PumpとHeaterがいるではないか。おそらくここに@Injectの秘密が隠されているのだろう。よくみると

HeaterProvider heaterProvider= Heater_Factory.create();
PumpProvider pumpProvider = Pump_Factory.create(Heater_Factory.create);

this.coffeeMakerMembersInjector = CoffeeMaker_MembersInjector.create(heaterProvider, pupmProvider)

となっており、Injectorとやらの引数にそれぞれ依存オブジェクトのProviderクラスを渡してやってるらしい。

そしてそのInjectorたちの中身がこれ。

public final class CoffeeMaker_MembersInjector implements MembersInjector<CoffeeMaker> {
  private final Provider<Heater> heaterProvider;

  private final Provider<Pump> pumpProvider;

  public CoffeeMaker_MembersInjector(Provider<Heater> heaterProvider, Provider<Pump> pumpProvider) {
    assert heaterProvider != null;
    this.heaterProvider = heaterProvider;
    assert pumpProvider != null;
    this.pumpProvider = pumpProvider;
  }

  public static MembersInjector<CoffeeMaker> create(
      Provider<Heater> heaterProvider, Provider<Pump> pumpProvider) {
    return new CoffeeMaker_MembersInjector(heaterProvider, pumpProvider);
  }

  @Override
  public void injectMembers(CoffeeMaker instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.heater = heaterProvider.get();
    instance.pump = pumpProvider.get();
  }

  public static void injectHeater(CoffeeMaker instance, Provider<Heater> heaterProvider) {
    instance.heater = heaterProvider.get();
  }

  public static void injectPump(CoffeeMaker instance, Provider<Pump> pumpProvider) {
    instance.pump = pumpProvider.get();
  }
}

createで渡されたProviderたちは、フィールドで保持される。そしていざ依存が必要とされた時

@Override
  public void injectMembers(CoffeeMaker instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.heater = heaterProvider.get();
    instance.pump = pumpProvider.get();
  }

  public static void injectHeater(CoffeeMaker instance, Provider<Heater> heaterProvider) {
    instance.heater = heaterProvider.get();
  }

  public static void injectPump(CoffeeMaker instance, Provider<Pump> pumpProvider) {
    instance.pump = pumpProvider.get();
  }

これらのメソッドが呼ばれ、実際に代入(注入)されていくのだろう。後述するSubComponentsMapで管理する時再び、このProviderFactoryと再会するというか、実装する。

インターフェースは@Injectできない

しかしまたここでハト君に欲望が生まれた。Pumpをクラスにせず、Interfaceとして実装して、サイホンポンプとかに実装させたらどうだろう。物は試しだ。......

interface Pump{
  void pumping();
}
class Thermosiphon implements Pump{
  private final Heater heater;
  
  @Inject
  Thermosiphon(Heater heater){
    this.heater = heater;
  }
  
  @Override
  public void pumping(){
    if(heater.isHot()){
      System.out.println("pumping")
    }
  }
}

できた。しかし、あれっ?さっきのCoffeeMakerクラス、確かフィールドは

@Inject Pump pump; //Thermosiphonではない

だったような。これってDagger2ではどうなるのだろう。またモズ君のところへ向かう。するとモズ君曰く、

@Injectなのだが

  • インターフェースでは使えない
  • サードパーティ製のクラスでは使えない。

ということだ。つまりPumpクラスをInterfaceにした場合は使えないということか...。詰んだな。Dagger使えないじゃん。

なんと、モズ君曰くそのための実装が別にあるという。先に言えよ。@Providesを使うらしい。詳しくは次のModuleで。

Module

まずは下を見てほしい。

@Provides static Pump providePump(Thermosiphon pump){
  return pump
}

このように@ProvidesをつけてPump型の戻り値をもつThermosiphonを提供することで、Dagger2がインジェクトできるようになるらしい。そして、このメソッドは慣例的に接頭語provideを、メソッドを実装するクラスは接尾語Moduleをつけ、さらにクラスの先頭に@Moduleをつける。

@Module 
class DripCoffeeModule{
 
 @Provides static Pump providePump(Thermosiphon pump){
   return pump
  }
}

しかし、これだけではまだ終わらない。Componentに手直しが必要なのだ。手直しはMainActivityのみで簡単である。

public class MainActivity extends AppCompatActivity {

    private CoffeeShop coffeeShop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        coffeeShop = DaggerMainActivity_CoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();
      
       coffeeShop.maker().drip()
    }
    
    @Component(modules = DripCoffeeModule.class)
    interface CoffeeShop{
      CoffeeMaker maker();
    }


}

Moduleの威力

Moduleを作ると、良いことがある。例えば、ハト君が急にHeaterクラスに普通のヒーターではなく、ElectricHeaterを使いたいと思っても大丈夫。次のようすれば良い。

@Module 
class DripCoffeeModule{
 
 @Provides static Pump providePump(Thermosiphon pump){
   return pump
  }
  
  //以下を追加すればDaggerが自動的に@InjectのついたHeaterにElectricHeaterを代入(注入)してくれる。
 @Provides static Heater provideHeater(){
   return new ElectricHeater();
 }
}

Provideメソッドを作るためにModuleを作るのはめんどくさいが、文字通りモジュール性を上げてくれるので、作っておいて損はない。

ここまでの実装2

ModuleComponentを実装して、やっと全体像ができた。さきほどまでの実装と変わったところのみを載せる。

public class MainActivity extends AppCompatActivity {

    private CoffeeShop coffeeShop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        coffeeShop = DaggerMainActivity_CoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();
      
       coffeeShop.maker().drip()
    }
    
    @Singleton
    @Component(modules = DripCoffeeModule.class)
    interface CoffeeShop{
      CoffeeMaker maker();
    }


}
@Module 
class DripCoffeeModule{
 
 @Provides static Pump providePump(Thermosiphon pump){
   return pump
  }
  
 @Singleton
 @Provides static Heater provideHeater(){
   return new ElectricHeater();
 }
}
public class ElectricHeater extends Heater{

  @Inject
  public ElectricHeater(){
  
  }
}

ここでComponentとprovideHeaterをSingletonにしている。理由はこうしないと、CoffeeMakerのインジェクトされたHeaterと、PumpにインジェクトされたHeaterが別のインスタンスなので、Pumpのコンストラクタに渡した、HeaterがisHot = trueにならないからだ

ちょっとした応用

Dagger2のComponentはApplicationに持たせるべし。

実際のアプリでは全体の親のComponent(今回はCoffeeShopのみ)をApplicationクラスに持たせることが多い。理由としてはApplicationがそのアプリにとっての寿命(Scope)となること、Activityなどで

((MyApplication)getApplication).getCoffeeShop()

をすれば、どこからでも呼べることなどが上げられる。これでどこでもコーヒーを飲めるようになった。

public class MyApplication extends Application{
  
  private CoffeeShop coffeeShop;
  
  @Override
  public void onCreate(){
    super.onCreate();
    
    coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();
  
  }
  
  public CoffeShop getCofeeShop(){
    return coffeeShop;
  }
}

なおmanifestでのアプリケーションの設定を忘れずに。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="パッケージの名前">

    <application
        android:name=".MyApplication"  <!--これの追加を忘れない。-->
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".home.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Scopeについて

いつものようにGithub眺めてたハト君はふと気づいたことがある。最近作ったCoffeeMakerのPumpとHeaterクラス、あれ呼ばれるたびにnewされるんだよな。であれば、WebからRetrofitとか使ってとってくるServiceクラスとかシングルトンにしたいやつ(何回もインスタンスを生成したくないやつ)はどうやって依存注入するのだろう。

これまたいつものようにモズ君に聞くと、それもアノテーションが解決してくれるらしい。

主に使うScopeアノテーションは次の1つくらい。

アノテーション 説明
@Singleton 寿命はComponentが死ぬまで。呼ばれるたびにnewせず、常に1つのインスタンスを返す。

Scopeの使い方

ModuleクラスのProviderComponentにそれぞれつけるだけ。

@Module 
class DripCoffeeModule{
 
 @Provides static Pump providePump(Thermosiphon pump){
   return pump
  }
  
 @Singleton //こんな感じ
 @Provides static Heater provideHeater(){
   return new ElectricHeater();
 }
}
 @Singleton
 @Component(modules = DripCoffeeModule.class)
    interface CoffeeShop{
      CoffeeMaker maker();
    }

独自のScopeの作り方

独自のスコープを作る方法もある。しかし下のスコープは別になんの役割もない。というのも上記@Singleton@ActivityScopeとしても何にも変わらないのだ。

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {
}

しかし、これはチームで作る際のScopeの目印となる。実際の実装では、Activityが終わる時Componentをクリアするようにして、スコープを擬似的に実装する。

必ず対応するComponentとModuleのScopeを合わせること。でないとエラーになります。

Scopeはなんのために必要か

次のSubComponentsを実装するのに重要。

SubComponents

(この内容は参考4のサイトの内容をつまみ食い日本語訳しています。)

ハト君は仕事熱心だった。彼のCoffeeMakerは莫大な進化を遂げ、DripCoffeeModuleはやばいことになっていた。依存の量が多すぎるのである。これはなんとか分割しなければ、結局メンテナンス性が下がってしまう。考えた末に3つの方法があることに気づいた。

  1. 1つのComponentに複数のModule
  2. Componentを階層構造にする(SubComponent)
  3. DependentComponent

1つのComponentに複数のModule

これはなかなかいい案に思えた。というか一番最初に思いついた。早速モズ君のところにいって相談してみると、渋い顔された。彼曰く、
そうやって作ったすべてModuleはみな同じScope、@Singletonなんだ」そうだ。Moduleの寿命がComponentと同じ寿命(Scope)であれば、すべてのModuleの寿命が変わらない。

ちなみに作り方としては2通りあり、

@Component(modules = {DripCoffee1Module.class, DripCoffee2Module.class})
public interface CoffeeShop

もしくは、DripCoffee1ModuleにDripCoffee2Moduleを持たせて、

@Component(modules = DripCoffee1Module.class)
public interface CoffeeShop
@Module(includes = DripCoffee2Module.class)
public class DripCoffee1Module

となる。しかし最初に説明した通り、独自のスコープを持たせることはできないし、また、@ActivityScope@FragmentScopeといったちょっとの間だけライフサイクルをもつものを作ることができない。

ここでモズ君による答え合わせが始まった。

Componentを階層構造にする(SubComponent)

DaggerFigure.jpg

というか、依存オブジェクトを全てApplicationの寿命がおわるまで持っておく必要ないよな。いいかえると、ApplicationのScopeである必要はない。モズ君が渋い顔したのはさっきの実装が、全ての依存を最後まで持つことになるからだ。

例えば、ActivityやFragmentを依存オブジェクトとして保持していたとする。これらはAppllicationよりもScopeが短い(小さい)。よって、それぞれComponentに分けて管理すれば、独自のScopeを定義して寿命が終われば捨てれば良い。そして、その方法を提供しているのが、SubComponentだ。SubComponentは親のComponentの依存オブジェクトを利用できる。

SubComponentは親のComponentのinnerクラスとして実装される。

たとえばCoffeeShopにはお客さんが必要である。

public class Customer {

    long id;
    Boolean isLogin = false;

    @Inject
    public Customer(){}

    public long id(){return  id;}
    public String login(){
        if (isLogin) {
            return "loginしています";
        }

        isLogin = true;
        return "loginしました";
    }
}

お客さんの寿命(来店してら退出するまで)を独自に定義する。

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomerScope {
}

Moduleをかく。

@Module
class CustomerModule {

    @CustomerScope
    @Provides
    static Customer provdeCustomer(){
        return new Customer();
    }
}

対応するComponentを書く。しかしここで、このComponentはCoffeeShop(Component)のSubComponentとする。なぜならCoffeeShopのお客さんだからだ。

@CustomerScope
@Subcomponent
public interface CustomerComponent {

    Customer enter();

    @Subcomponent.Builder
    static interface Builder{
        CustomerComponent build();
    }
}

必ず対応するComponentとModuleのScopeを合わせること。でないとエラーになります。

SubComponentたちをまとめるModuleを作る。

@Module(subcomponents = {CustomerComponent.class})
public class HogeModule {
}

まとめたModuleをCoffeShop(Component)に追記。

@Singleton
    @Component(modules = { DripCoffeeModule.class, HogeModule.class})
    interface CoffeeShop{
        CoffeeMaker open();
        CustomerComponent.Builder customerBuilder();

    }

これで使えるようになった。

DependentComponent

Applicationより短いScopeをもつComponentを作るもう一つの方法。
@Componentにdependenciesフィールドで追加する。

依存Componentは依存先のComponentをinterfaceを介してアクセスする。

ここは独自に調べて欲しい。

使い分け

  • 2つのComponentを独立に、癒着させないように保ちたい場合、DependentComponentを使う。
  • 2のComponentがApplicationActivityのような繋がりを持つ場合はSubComponentを使う。またdagger-androidはSubComponentと相性がよく、ボイラープレートを減少させる。

ここまでの実装。

かなり分量が多くなったのでGitHubにまとめておく。
GitHubサンプル

ModuleBinding

むかしがたり

(この内容は参考3のサイトをほぼ日本語解釈したものとなります。)

ハト君はDagger2にだんだん慣れてきたのだが、MultiBindingというところで悩んでいた。するとモズ君が昔話を始めた。

時は遡り、versions2.7以前のこと
SubComponentの作り方はこんな感じだった。

@Singleton
@Component(
    modules = {
        AppModule.class
    }
)
public interface AppComponent {
    MainActivityComponent plus(MainActivityComponent.ModuleImpl module);

    //...
}

この記述によって、DaggerはMainActivityComponentAppComponentからのアクセスを持つことをしることができた。

MainActivityにおけるインジェクションも似た感じのコードだった。

@Override
protected ActivityComponent onCreateComponent() {
    ((MyApplication) getApplication()).getComponent().plus(new MainActivityComponent.ModuleImpl(this));
    component.inject(this);
    return component;
}

ここで問題が起きた。ActivityはAppComponentに依存している。SubComponentを呼ぶのに、いつも親のComponentにアクセスしなければならない。また、AppComponentは全てのSubComponentをplusメソッドで宣言しなければならない。ああなんて面倒なんだ。そして親と子が癒着しすぎている気もする。

そこで迷えるAndroiderたちに新しい道が示された。

@Moduleにsubcomponentsフィールドが追加されたのだ。そしてこのActivityBindingModuleを親のComponentにmodulesフィールドとして書けば良い。


@Module(
        subcomponents = {
                MainActivityComponent.class,
                SecondActivityComponent.class
        })
public abstract class ActivityBindingModule {
    //...
}

ActivityBindingModuleAppComponentにインストールされる。つまり、MainActivitySecondActivityAppComponentのSubComponentになるのだ。このほうほうで実装されたSubComponentはAppComponentに明示的に書かなくてよい(plusしなくてもよい)。

ちなみに先ほどの CoffeeShopの実装でHogeModuleが今回のBingdingModuleにあたる。ここで、新たな機能Mapを使うことで先ほどの問題が解決していく。

Mapを使ってインジェクトしやすくする。

今回の内容はそれほど難しくない。ゆっくり追っていけば理解できるはず。

まず、は親のComponentたちのサンプル。

@Component(modules = ParentModule.class)
interface ParentComponent {
  Map<String, String> stringMap(); //フィールドにMapを保持
  ChildComponent childComponent(); //SubComponentを保持
}

@Module
class ParentModule {
 
  @Provides @IntoMap //IntoMapでMapに入れてくれる。
  @StringKey("a")    // Mapから取得するためのkey
  static String stringA() {
    "parent string A";
  }

  @Provides @IntoMap
  @StringKey("b")
  static String stringB() {
    "parent string B";
  }
}

重要な部分としては@IntoMap@StringKeyである。これをつけることで後ほど

parentComponent.stringMap.get(/*先ほどのキーワード*/)

で取得することができる。使い方はjavaのMapそのままである。
次に子供のSubComponentたちのサンプル。

@Subcomponent(modules = ChildModule.class)
interface ChildComponent {
  Set<String> strings();
  Map<String, String> stringMap();
}

@Module
class ChildModule {

  @Provides @IntoMap
  @StringKey("c")
  static String stringC() {
    "child string C";
  }

  @Provides @IntoMap
  @StringKey("d")
  static String stringD() {
    "child string D";
  }
}

さいごにこれらを使ったテスト。


@Test void testMultibindings() {
  ParentComponent parentComponent = DaggerParentComponent.create();
  assertThat(parentComponent.strings()).containsExactly(
      "parent string 1", "parent string 2");

  ChildComponent childComponent = parentComponent.childComponent();
  assertThat(childComponent.stringMap().keySet()).containsExactly(
      "a", "b", "c", "d");
}

ちなみにMapのほかSetも提供されている。
公式サイト

また、独自のKeyMapを作ることができる。それは、さらなる理解のためにのサイトを参照してほしい。

@Bindsを使って記述量をへらす

さきほど

@Module
class CustomerModule {

    @CustomerScope
    @Provides
    static Customer provdeCustomer(){
        return new Customer();
    }
}

と書いたが、次のようにもかける。

@Module
abstract class CustomerModule{

    @CustomerScope
    @Binds
    abstract Customer provideCustomer()
}

すこしだけ記述量が減った。

独自のInjectorを作る。

工事中

さらなる理解のために

こちらのYukiさんのサイトがここまでのレベルを理解したのであれば読めるのではないでしょうか。
Dagger2. MultibindingでComponentを綺麗に仕上げる

参考

下3つを参考に書かせていただきました。一番下のやつは英語ですが、なぜSubComponentを使うのか Scopeの重要性などとてもわかりやすいです。

  1. Dagger2の公式ドキュメントを読んで基礎を理解してみる
  2. Dagger2のscopeの使い方を正しく理解する
  3. Activities Subcomponents Multibinding in Dagger 2
  4. Dagger 2 : Component Relationships & Custom Scopes

誤字認識ミスなどの指摘のお願い

ハト君と同様に、最近Dagger2に触り始めたばかりです。もし誤字や認識ミスなどありましたら、ご指摘をよろしくお願いします。

184
128
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
184
128

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?