きっかけ
Dagger2はAndroid Developersの Guide to App Architecture でも紹介されており
そろそろAndroidエンジニアでDagger2やDIがわからないのはまずそうな気がしてきたので、調べてみました。
よく分からないところは対応する【補足メモ】を読んでみてください。ソースコードを載せているので、イメージしやすいかもしれません。
最近アップデートがあったAndroidとの関連(dagger.android)までは今回は触れることができていません。
Dagger初心者の自分が驚いたところを太字にしておきます。
ほぼ以下のドキュメントの意訳です。間違っているところはご指摘下さい。
User's Guide
https://google.github.io/dagger/users-guide.html
User's Guide
Dagger2概要
いろんなアプリの良いクラスとして、BarcodeDecoderはきっとBarcodeCameraFinder, DefaultPhysicsEngine,や HttpStreamerみたいなクラスの依存関係を持っているでしょう。
対比して、悪いクラスの例として、BarcodeDecoderFactoryなど何もしていないのにスペースを取っているクラスがあります。
これをdependency injection design patternを使って置き換えることができます。
標準の javax.inject annotations (JSR 330)を使って組み立てることにより、テストを簡単にすることができます。FakeのWeb APIを叩くためのクラスに入れ替えるような実装は必要ないです。
**Dependency injectionはテストのためだけではありません。再利用や交換しやすいモジュールを作ることができます。**例えばAuthenticationModuleをアプリ内で共有して利用できたり、デバッグではDevLoggingModuleを使って、ProdLoggingModuleをプロダクションで利用するしたりすることができます。
なぜDagger2は他のDIとは違うか?
DIのライブラリはたくさんこれまでありました。なぜ車輪の再開発をしたのか? Dagger2はコード生成により完全に実装した初めてのものです。指針としては依存関係の注入ができるだけ簡単で追跡可能で、実行可能なコードを生成することです。
依存関係の宣言
Daggerはアプリケーションのクラスのインスタンスを構築(Construct)し、その依存関係を満たします。
コンストラクタやフィールドを特定するのにjavax.inject.Inject annotationを利用します。
Daggerがクラスをインスタンス化する時に@Inject
アノテーションを使っているコンストラクタを使います。新しいインスタンスが必要になった時に、Daggerが必要なパラメータを入手して、コンストラクタを呼び出します。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
またDaggerはフィールド(メンバ変数)を直接注入することもできます。この例ではheaterフィールドと、pumpフィールドのインスタンスを入れます。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
もし**@Inject
がついたフィールドがあって、@Inject
がついたコンストラクタがない場合、Daggerは要求されればフィールドを作成するが、インスタンスは作成しようとしません。(おそらく今回のCoffeeMakerのような形。)何も引数がないコンストラクタを@Inject
をつけて作ることで、同じようにインスタンスを作ってくれます。**
Daggerはmethod injectionもサポートしているが、フィールドやコンストラクタで利用するのが一般的。
@Inject
がないクラスはDaggerで構築(Construct)することはできない。
【補足メモ】 依存関係の宣言
コードの断片だったので、わかりにくいですよね、、
こういうイメージです。(コードはここにあります。https://github.com/takahirom/dagger2-sample/commit/4b4e7a047dee0993735c6605bc06bdcd3084c8b8 )
@Component
周辺については、後ほど紹介しますが、Componentを作って、maker()を呼び出します。
maker()を呼び出すと、何が起こるかというと、これだけで、new CoffeeMaker()されて、そのCoffeeMakerのフィールドにnew Heater()されて注入されたインスタンスが返ってきます。
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.create();
System.out.println(coffeeShop.maker());
}
@Component
interface CoffeeShop {
CoffeeMaker maker();
}
}
@Inject
によってコンストラクタをDaggerから実行できるようにして、@Inejct
でフィールドも指定しています。
class CoffeeMaker {
@Inject
Heater heater;
@Inject
public CoffeeMaker() {
}
}
フィールドからHeaterも作成されます。
class Heater {
@Inject
public Heater() {
}
}
今回はフィールド(メンバ変数)に@Inject
を利用していましたが、
コンストラクタの引数につけると先に引数のオブジェクトをnewして、それを利用してコンストラクタを呼び出してくれたりします。
依存関係を満足させる
デフォルトではDaggerは要求された型を構築(Construct)することで依存関係を満たします。CoffeeMakerを要求した時new CoffeeMaker()
してその注入できるフィールドを設定します。
しかし@Inject
はどこでも使えるわけではありません。
- interfaceはインスタンス化することができません
- サードパーティーのクラスはアノテーションをつけることができません。
- 設定可能(Configurable)なオブジェクトは設定していなくてはいけない。
このような場合は @Provides
を使ったメソッドを依存関係を満足させることができます。メソッドの返り値は依存を満たす型にしてください。
例えばprovideHeaderメソッドはHeaterが必要な時に呼ばれます。
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
また@Provides
がついたメソッドはメソッド自身が依存関係を持つことができます。Pumpが必要なところでThermosiphonをを返す例です。
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
@Provide
がついたメソッドはモジュールに属する必要がある。
ただクラスに@Module
アノテーションをつける。
@Module
class DripCoffeeModule {
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
}
慣習として@Provides
メソッドはprovideから始まるメソッド名にして、モジュールはModuleで終わるクラス名にする。
【補足メモ】 依存関係を満足させる
以下の関連(Graph)を構築するのほうにまとめます。
関連(Graph)を構築する
@Injection
や@Provides
がついたクラスは依存関係によって構築された関連(Graph)を形成する。
アプリケーションのmain()メソッドやAndroidのApplicationクラスのようなCalling codeで"明確に定義された関連のルートのセット"を介してその関連(Graph)にアクセスします。Dagger2ではそのセットは、ほしいクラスが返り値の引数がないメソッドがあるインターフェースで宣言される。@Component
アノテーションを適応して、アノテーションのmodulesにモジュールを渡すことでそのモジュールを使って実装を完全に作成します。
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
Daggerと最初についたComponentのためのクラスが生成されるので、そこからCoffeeShopのインスタンスが作成できます。
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();
もしインナークラスになっていれば、こういう形で_区切りで生成される
以下の場合はDaggerFoo_Bar_BazComponentという名前になる。
class Foo {
static class Bar {
@Component
interface BazComponent {}
}
}
このdripCoffeeModule()はDripCoffeeModuleクラスがデフォルトのコンストラクタがアクセスできる(作られている必要はない)のであれば、必要ない。また@Provides
がついているメソッドが全部staticであれば、コンストラクタがアクセスできる必要もない。
DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
もしモジュールのインスタンスが必要ない場合は、builderを使う代わりに以下のようにcreate()
メソッドを使うことが出来る。
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
これでDaggerが完全に依存性が注入されたCoffeeMakerを取得できる実装が入ったCoffeeShopをシンプルに使うことができます。
public class CoffeeApp {
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
coffeeShop.maker().brew();
}
}
【補足メモ】 依存関係を満足させる
今回もコードの断片だったのでわかりにくい部分があったかもしれませんので、全コード載せてみますね。(ここにコードがあります。 https://github.com/takahirom/dagger2-sample/commit/7adcffb7b4b38d1aede06645df9bbdc37603bd23 )
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();
System.out.println(coffeeShop.maker());
}
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
@Module
static class DripCoffeeModule {
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
}
}
class CoffeeMaker {
@Inject
Heater heater;
@Inject
Pump pump;
@Inject
public CoffeeMaker() {
}
}
現在、以下のような何もない@InjectだけのクラスがHeater,Pump,ElectricHeater,Thermosiphon用意してある形になります。
public class ElectricHeater extends Heater {
@Inject
public ElectricHeater(){
}
}
@Module
を利用することにより、それぞれのインスタンスが利用されていることがわかります。
Graphのバインディング
今回の例は典型的なバインディングを使って、コンポーネントを構築する例を示しているが、関連(Graph)のバインディングを助けるさまざまな仕組みがある。
以下は依存関係を構築する際に利用することができ、良い形でコンポーネントを作成するのに利用される。
(以下があるようですが、全ては理解できていません)
-
@Component.modules
から参照されていて、かつ@Module
のついている or@Module.includes
から推移的に参照されるモジュールの@Provides
アノテーションがついているメソッド -
@Inject
がついているコンストラクタで、@Scope
がついていないか、@Scope
がついていてコンポーネントのスコープにあっているもの。 - component dependenciesのcomponent provision methods(調べられていません。)
- component自身から入れられるもの
- サブコンポーネントを含むqualify annotationがついていないBuilder
- 上記のProviderやLazy
- Provider>
- MembersInjector
SingletonとScoped Bindings
@Singleton
は、@Provides
がついたメソッドか注入可能(Injectable)なクラスにつける。Graph全ての利用者に一つのインスタンスを使う。
@Provides @Singleton static Heater provideHeater() {
return new ElectricHeater();
}
クラスへの@Singleton
はドキュメントとしても役立ちます。このクラスが複数のスレッドで共有される可能性があることが分かります。
@Singleton
class CoffeeMaker {
...
}
Dagger2はGraph内のスコープ付き(@Singleton
を含む)インスタンスとコンポーネントの実装のインスタンスを結びつけているため、コンポーネントにスコープを設定する必要がある。例えば同じコンポーエントに@Singletonのbinding
(Graph内のオブジェクトとの結びつけという意味だと思われる)と@RequestScoped
(カスタムで定義したScope) bindingを同時に持たせる意味はない。なぜならライフサイクルの異なるコンポーネントに存在する必要があります。カスタムで定義したScopeを作るにはComponentにアノテーションを適応するだけです。
@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
CoffeeMaker maker();
}
【補足メモ】 SingletonとScoped Bindings
Singletonのインスタンスはどういう感じで管理されるか?(ここにコードがあります
https://github.com/takahirom/dagger2-sample/commit/063b7f45c9fb97e54b90ce56a4f96b217e39a6bc )
Dagger2はGraph内のスコープ付き(@Singleton
を含む)インスタンスとコンポーネントの実装のインスタンスを結びつけている。とはどいういうことか?
どういう感じで管理されるのか?
簡単な例で、@Singleton
を使って実験してみました。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final CoffeeShop coffeeShop = DaggerMainActivity_CoffeeShop.create();
System.out.println(coffeeShop.maker());
System.out.println(coffeeShop.maker());
final CoffeeShop coffeeShop2 = DaggerMainActivity_CoffeeShop.create();
System.out.println(coffeeShop2.maker());
System.out.println(coffeeShop2.maker());
}
@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
}
@Singleton
class CoffeeMaker {
@Inject
Heater heater;
@Inject
Pump pump;
@Inject
public CoffeeMaker() {
System.out.println("new CoffeeMaker()");
}
}
これで以下が出力されます。つまり@Singleton
でCoffeeShopに関連付けられていそうです。
new CoffeeMaker()
com.github.takahirom.dagger_simple.CoffeeMaker@a69a8a4
com.github.takahirom.dagger_simple.CoffeeMaker@a69a8a4
new CoffeeMaker()
com.github.takahirom.dagger_simple.CoffeeMaker@336f60d
com.github.takahirom.dagger_simple.CoffeeMaker@336f60d
この状態でメモリのダンプをとってみました。
今回の実装では、MainActivity.onCreate()がおわるとCoffeeMakerへの参照が消える、つまりインスタンスが消えます。
ActviityでCoffeeShopのインスタンスを持っている場合はどうなるか?
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();
System.out.println(coffeeShop.maker());
System.out.println(coffeeShop.maker());
}
CoffeeShopのinterfaceを実装しているDaggerにより自動生成されたDaggerMainActivity_CoffeeShopクラスのフィールドにあるcoffeeMakerProviderによってインスタンスが保持されていることで、インスタンスを保持するようです。
Dagger2はGraph内のスコープ付き(@Singleton
を含む)インスタンスとコンポーネントの実装のインスタンスを結びつけているの意味が何となくわかりました。
また@Singletonなしでも実験してみたのですが、普通にCoffeeMakerのインスタンスは消えていました。
再利用可能なスコープ(Reusable scope)
インスタンス化の回数を抑えたいが、1つに制限するまでもないことが時々あります。例えばAndroidではインスタンス化は高価な処理です。
そのようなとき、@Reusable
を使うことができます。@Reusable
は他のスコープと違って、1つのコンポーネントと結びつきません。その代わりにコンポーネントはキャッシュされたインスタンスを返すか、インスタンス化して返します。
...
(あんまり使われていない機能みたいなので、今回は省略します)
解放可能なリファレンス(Releasable references)
...(こんな感じで実装するみたいなのですが、あんまり使われていないみたいなので、今回も省略します)
@Inject @ForReleasableReferences(MyScope.class)
ReleasableReferences myScopeReferences;
void lowMemory() {
myScopeReferences.releaseStrongReferences();
}
...
Lazy injections
ときには遅延してインスタンス化する必要がることもあります。任意のbinding Tに対してLazyのget()メソッド呼び出しまで、インスタンス化を遅らせることができます。Tがシングルトンの場合はObjectGraph内が全て同じインスタンスになる。シングルトンでなければ各注入箇所でインスタンス化されます。しかし二回目以降は同じインスタンスを返します。
class GridingCoffeeMaker {
@Inject Lazy<Grinder> lazyGrinder;
public void brew() {
while (needsGrinding()) {
// Grinder created once on first call to .get() and cached.
lazyGrinder.get().grind();
}
}
}
Provider injections
一つの値を注入する代わりにいくつもインスタンスが必要になるときがあります。FactoryやBuilderを使うなどいくつかの選択肢があるが、一つのオプションとして、ただのT型ではなく、Providerを利用することができます。Providerはget()メソッドが呼ばれるたびにbindingロジックを呼び出します。そのbindingロジックが@Injectがついたコンストラクタであれば新しいインスタンスが作られますが、@Provides
がついたメソッドによってインスタンスが作られる場合はそのような保証はありません。
class BigCoffeeMaker {
@Inject Provider<Filter> filterProvider;
public void brew(int numberOfPots) {
...
for (int p = 0; p < numberOfPots; p++) {
maker.addFilter(filterProvider.get()); //new filter every time.
maker.addCoffee(...);
maker.percolate();
...
}
}
}
注意: Providerを注入するコードは混乱するコードが作成される可能性があり、誤ったスコープのオブジェクトや誤ったスコープや誤った構造のオブジェクトがGraphに使われてしまう可能性があります。多くの場合、ファクトリやLazyを利用するか、コードのライフタイムと構造を再構成して、Tを注入することができます。場合によってはProviderは効率的に実装することができます。一般的な使い方はオブジェクトの自然なライフタイムにそっていないレガシーなアーキテクチャを利用する必要がある場合です。
(Architecture Componentのサンプルアプリでも使われていました https://github.com/googlesamples/android-architecture-components/blob/e33782ba54ebe87f7e21e03542230695bc893818/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java#L30 )
Qualifiers
時には型だけでは、依存関係を特定するのに不十分です。例えば洗練されたコーヒーメーカーでは水用のヒーターと、ホットプレート用のヒーターを分けたいです。
この場合、qualifier annotationを追加します。これは@Qualifier
を持つアノテーションです。javax.injectに含まれるqualifier annotationの@Named
の宣言は以下のようになっています。
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
独自のqualifier annotationを作ることも、@Namedを利用することも可能です。フィールドか引数にアノテーションを適応することで限定することが可能です。型とqualifier annotationは依存関係を識別するために利用されます。
class ExpensiveCoffeeMaker {
@Inject @Named("water") Heater waterHeater;
@Inject @Named("hot plate") Heater hotPlateHeater;
...
}
対応する@Providesメソッドに注釈をつけることによって指定された値を供給します。
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}
@Provides @Named("water") static Heater provideWaterHeater() {
return new ElectricHeater(93);
}
依存関係は複数のqualifier annotationをもたない可能性があります。(?)
【補足メモ】Qualifiers
実際に利用してみて、namedで別の名前を入れてみたらどうなるのか見てみました。(ここにコードがあります https://github.com/takahirom/dagger2-sample/commit/fbabf72a454ec7a96628783a81e12bcb765d6cad )
@Module
static class DripCoffeeModule {
@Provides
@Named("water")
static Heater provideHeater() {
return new ElectricHeater();
}
class CoffeeMaker {
@Inject
@Named("hot plate")
Heater heater;
...
}
ちゃんとエラーとして検出してくれていて、コンパイル時にちゃんと警告してくれるようです。
MainActivity.java:30: エラー: @javax.inject.Named("hot plate") com.github.takahirom.dagger_simple.Heater cannot be provided without an @Provides- or @Produces-annotated method.
CoffeeMaker maker();
^
@javax.inject.Named("hot plate") com.github.takahirom.dagger_simple.Heater is injected at
com.github.takahirom.dagger_simple.CoffeeMaker.heater
com.github.takahirom.dagger_simple.CoffeeMaker is provided at
com.github.takahirom.dagger_simple.MainActivity.CoffeeShop.maker()
エラー1個
以下のようにちゃんとwaterにしたらコンパイルできるようになりました。
class CoffeeMaker {
@Inject
@Named("water")
Heater heater;
Optional bindings
...(Java8やGuavaのOptionalとDaggerを使う時の機能のようなので省略させていただきます。)
Binding Instances
多くの場合、コンポーネントをビルドする時にデータを利用することができます。例えば、コマンドライン引数を利用するアプリケーションの場合を考えてみましょう。コンポーネント内にそれらの引数をバインドしたいと思うかもしれません。
アプリケーションは@UserName
というアノテーションで注入したい、ユーザーの名前を表す引数(String)を取るとします。@BindsInstance
をコンポーネントビルダーに追加して、そのインスタンスをコンポーネントビルダーに注入できるようにすることができます。
@Component(modules = AppModule.class)
interface AppComponent {
App app();
@Component.Builder
interface Builder {
@BindsInstance Builder userName(@UserName String userName);
AppComponent build();
}
}
そしてアプリはこのような形になります。
public static void main(String[] args) {
if (args.length > 1) { exit(1); }
App app = DaggerAppComponent
.builder()
.userName(args[0])
.build()
.app();
app.run();
}
上記の例では、@UserName
Stringをコンポーネントに注入すると、 このメソッドを呼び出す時にBuilderに提供されたインスタンスが使用されています。(? このメソッドがどこかわからなかった、、) コンポーネントをビルドする前に全ての@BindsInstance
メソッドを呼び出す必要があります。以下の@Nullable
バインディングは例外です。
@BindsInstance
メソッドのパラメータが@Nullable
とマークされている場合、バインディングは@Provides
メソッドと同じ方法で"nullable"とみなされます。そのため、注入される側も@Nullable
であり、nullが受け入れられる必要があります。さらにBuilderのメソッド呼び出しを省略することができ、そのときコンポーネントはインスタンスがnullだとして扱います。
@BindsInstance
メソッドは@Module
にコンストラクタで渡してすぐに適応するよりも優先して利用されるべきです。
(こちらもArchitecture Componentsのサンプルで https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java#L38 利用されているようです。 )
【補足メモ】Binding Instances
ほぼそのままなのですが、コードを見ていきましょう(ここにコードがあります https://github.com/takahirom/dagger2-sample/commit/c2cbfdbbf883203378ad51fd1d629ea25a7dbe6f )
例の中でUserNameになっていたものはQualifierで良いようです。なので@Namedで利用しています。
今回はアプリ起動時に砂糖を使うかどうかを渡せるようにしてみました。
suger(true)で砂糖を使うように指定しています(自分は砂糖入れます)
coffeeShop = DaggerMainActivity_CoffeeShop.builder().sugar(true).build();
System.out.println(coffeeShop.maker());
例と同じようにしています。
@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
@Component.Builder
interface Builder {
@BindsInstance
Builder sugar(@Named("sugar") boolean isAddingSugger);
CoffeeShop build();
}
}
sugerを入れたいところに入れます。
class CoffeeMaker {
...
@Inject
@Named("sugar")
boolean isAddSugar;
コンパイル時のバリデーション
...(Daggerのアノテーションプロセッサーは厳格で間違っていたらエラーを出す、といったことなので省略します。)
コンパイル時のコード生成
DaggerのアノテーションプロセッサーはCoffeeMaker_Factory.javaやCoffeeMaker_MembersInjector.javaといったクラスを自動生成します。それらのクラスはDaggerの実装の詳細です。注入を通してデバッグするときには便利ですが、あなたはそれらを直接利用する必要はありません。コードで必要なものはコンポーネント用のDaggerで始まるものだけです。
ビルドでDaggerを使う方法
...(結局Androidだったりするので省略します。)
まとめ
確かに使いこなせれば便利そうな気はしました。特にあるクラスのインスタンスをDaggerで作ると、そのフィールドやメソッドの引数の作成などインスタンスを作るのが連鎖?していくので、とても便利な感じがありました。
ただ最初はちょっと学習コストが重めな気がするので、このQiitaでちょっと楽になる人がいるといいなと思っています。