とてもつもなくわかりやすい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
ここまでの実装を実際に動かしてみる。@Component
やDaggerMainActivity_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();
}
これらのメソッドが呼ばれ、実際に代入(注入)されていくのだろう。後述するSubComponents
をMap
で管理する時再び、このProvider
やFactory
と再会するというか、実装する。
インターフェースは@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
Module
とComponent
を実装して、やっと全体像ができた。さきほどまでの実装と変わったところのみを載せる。
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
クラスのProvider
とComponent
にそれぞれつけるだけ。
@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つのComponentに複数のModule
- Componentを階層構造にする(SubComponent)
- 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)
というか、依存オブジェクトを全て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が
Application
とActivity
のような繋がりを持つ場合は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はMainActivityComponent
がAppComponent
からのアクセスを持つことをしることができた。
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 {
//...
}
ActivityBindingModule
はAppComponent
にインストールされる。つまり、MainActivity
と SecondActivity
はAppComponent
の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の重要性などとてもわかりやすいです。
- Dagger2の公式ドキュメントを読んで基礎を理解してみる
- Dagger2のscopeの使い方を正しく理解する
- Activities Subcomponents Multibinding in Dagger 2
- Dagger 2 : Component Relationships & Custom Scopes
誤字認識ミスなどの指摘のお願い
ハト君と同様に、最近Dagger2に触り始めたばかりです。もし誤字や認識ミスなどありましたら、ご指摘をよろしくお願いします。