TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

最小構成でDagger2の使い方の初歩を理解する with kotlin

対象者

  • DIはもうわかったけどDagger2の使い方がわからない人
  • javaのサンプルは多いけどkotlinのサンプルが少なくて困惑している人
  • @Moduleとか@Provideとか@Componentとか@Injectってあるけど最小で動かすとしたらどれが必要なんだと困惑している人

最小構成(パターン①)

以下の3クラスで構成する

CoffeeMaker.kt

import javax.inject.Inject

class CoffeeMaker @Inject constructor(private val heater: Heater) {

    fun getTemperature() : String {
        return heater.getTemperature()
    }
}

Heater.kt

import javax.inject.Inject

class Heater @Inject constructor() {

    fun getTemperature() : String {
        return "38度"
    }
}

MainActivity.kt

import dagger.Component

class MainActivity : AppCompatActivity() {

    private lateinit var coffeeShop :CoffeeShop

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val dataBinding = DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity)

        // DaggerCoffeeShopはComponent interfaceを継承したクラス。
        // Dagger2によってビルド時に自動生成される。
        coffeeShop = DaggerCoffeeShop.create()

        // ビルド時に自動生成されたクラスがDIしてくれているので、
        // maker()実行だけで、メンバにインジェクト済みのインスタンスが取得できる
        val coffeeMaker = coffeeShop.maker()

        // 38度が返ってくる
        dataBinding.textView.text = coffeeMaker.getTemperature()
    }

}

@Component
interface CoffeeShop {
    fun maker(): CoffeeMaker
}

解説:パターン①

Dagger2は一行で書くと
"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる"
ツールだと理解した。

今回自動生成されたDaggerCoffeeShopは以下のような形になる。

public final class DaggerCoffeeShop implements CoffeeShop {
  private DaggerCoffeeShop(Builder builder) {}

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

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

  @Override
  public CoffeeMaker maker() {
    return new CoffeeMaker(new Heater());
  }

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

    public CoffeeShop build() {
      return new DaggerCoffeeShop(this);
    }
  }
}

以下行でわかるとおり、@Componentを付けたinterfaceに対する実体クラスが自動生成されている。

public final class DaggerCoffeeShop implements CoffeeShop {

また、以下の行ではmaker()の返却インスタンスであるCoffeeMakerインスタンスを生成する コードが自動生成されている。

  @Override
  public CoffeeMaker maker() {
    return new CoffeeMaker(new Heater());
  }

これによってDIを考慮して設計されたクラスをインスタンス化したいときに、クラス生成地獄を隠蔽することが可能になっている。

最小構成(パターン②)

"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる" のはわかった。 しかしActivityが複数のメンバ変数を持つような場合は一つ一つ上記のような書き方で注入していく必要があるのだろうか。

Dagger2ではもうひとつ書き方が用意されている。 "自分自身の@Injectが記載されているメンバ変数全てにインスタンスを設定してくれる"方法である。

以下に対象コードを記載する。

class OtherActivity : AppCompatActivity() {

    @Inject lateinit var coffeeMaker : CoffeeMaker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = DataBindingUtil.setContentView<OtherActivityBinding>(this,R.layout.other_activity)

        DaggerActivityComponent.create().inject(this)

        binding.textView.text = coffeeMaker.getTemperature()
    }
}

@Component
interface ActivityComponent {
    fun inject(activity:OtherActivity)
}

解説:パターン②

先ほどと異なるのは @Componentが記載されているインターフェースのメソッドに自分自身が設定されていることである。 このような場合、以下のようなクラスが自動生成される。

public final class DaggerActivityComponent implements ActivityComponent {
  private DaggerActivityComponent(Builder builder) {}

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

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

  private CoffeeMaker getCoffeeMaker() {
    return new CoffeeMaker(new Heater());
  }

  @Override
  public void inject(OtherActivity activity) {
    injectOtherActivity(activity);
  }

  private OtherActivity injectOtherActivity(OtherActivity instance) {
    OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker());
    return instance;
  }

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

    public ActivityComponent build() {
      return new DaggerActivityComponent(this);
    }
  }
}

injectメソッドを実行すると、injectOtherActivityが呼ばれている。 ここでは以下の処理が行われている。

OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker());

コードを追うと以下のようになっている。

public final class OtherActivity_MembersInjector implements MembersInjector<OtherActivity> {
  private final Provider<CoffeeMaker> coffeeMakerProvider;

  public OtherActivity_MembersInjector(Provider<CoffeeMaker> coffeeMakerProvider) {
    this.coffeeMakerProvider = coffeeMakerProvider;
  }

  public static MembersInjector<OtherActivity> create(Provider<CoffeeMaker> coffeeMakerProvider) {
    return new OtherActivity_MembersInjector(coffeeMakerProvider);
  }

  @Override
  public void injectMembers(OtherActivity instance) {
    injectCoffeeMaker(instance, coffeeMakerProvider.get());
  }

  public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) {
    instance.coffeeMaker = coffeeMaker;
  }
}

呼び出されている以下のメソッドを見ると、指定したインスタンスに生成されたインスタンスを設定している。

public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) {
    instance.coffeeMaker = coffeeMaker;
  }

このような仕組みで自身のメンバ変数にDIする書き方も可能である。

まとめ

・ ひとまずDagger2を利用してインスタンス生成までは実行できた。

・ しかしDagger2を利用して本来やりたいことは自動テスト時に対象クラス以外をモッククラスに置き換えることである。 そのためにはModuleとprovideを理解する必要がある。

・ 全ソースコードココ参照

おまけ:自分が理解するうえで詰まった(困惑した)ポイントまとめ

1. なんかいろいろ書き方があってわからない
→指定したクラスの依存性を満たしたインスタンスを返却する方法と、自分自身を指定してメンバに注入する方法が混ざって混乱していた。

2. 注入される側(今回の例で言えばHeaterクラス)のコンストラクタには@Injectいらなくない?注入されるだけだし。
→記入しないと注入するクラスだと認識してくれなくてコードが自動生成されないみたい。

3. RebuildしてもDaggerXXXクラスが自動生成されない....
→MakeProjectしたら動作するようになった。謎。

4. なぜかフィールドインジェクション作成時ビルドが成功しなかった。
→メンバ変数がprivateだとinjectするタイミングで参照できないために、自動生成に失敗していたみたい。privateメンバ変数に値を入れたい場合はコンストラクタインジェクションしかないかも。