square 社製の Dagger を fork して生まれた Dagger2(Google Dagger) は、リリース当初は導入事例があまり見られず手探りな状態が続いていましたが、最近ではプロダクトへの導入事例を聞くことも珍しくなくなりました。僕が関わっているいくつかのプロダクトでも Dagger2 を積極的に利用しており、もはやなくてはならない存在になったと言っても過言ではありません。
ところで、 Dagger2 は形式に沿った Module と Component のコードを書くことで、コンパイル時に依存注入するためのコードを生成し、アプリケーションが依存注入を要求した時に処理を実行するものです。この処理は同期的に行われますが、もしこの処理自体を非同期にしたい場合はどうすればよいのでしょうか。例えば、APIを叩くなどの処理が絡んでしまい、同期的に依存を解決できない場合。また依存解決に必要な処理をどうしてもメインスレッドで待つことが出来ない場合や、パフォーマンスの関係上非同期のほうが都合が良い、などといった場面の場合。我々エンジニアは、どのような書き方をすべきなのでしょうか。
その答えは既に Dagger2 に用意されています。Producers という仕組みを利用するのです。
目次
はじめに
Dagger2 には Producers という非同期で依存を解決する仕組みがあります。
通常の Inject とは違い、@Inject
アノテーションを使うことはありません。
Component は ListenableFuture<T>
を返すメソッドを定義するようにし、 <T>
を注入して欲しいクラスはそのメソッドを呼び出します。そして非同期にインスタンスを受け取るのです。
基本的な使い方
// dagger2 compile 'com.google.dagger:dagger:2.5' compile 'com.google.dagger:dagger-producers:2.5' // これが必要 apt 'com.google.dagger:dagger-compiler:2.5' androidTestApt 'com.google.dagger:dagger-compiler:2.5' provided 'javax.annotation:jsr250-api:1.0'
Producers は Dagger2 を拡張したものですので、追加の dagger-producers
を読み込んでおく必要があります。
簡単な例です。 String を返す Module を考えてみます。記述のポイントは以下のとおりです。
@ProducerModule
を使うことになります。userName(UserData userData)
が注入するインスタンスを返すメソッドです。provideUserData()
はUserData
を生成して返しますが、そのまま返すのではなく、ListenableFuture<UserData>
を返すようにします。
@ProducerModule final class AppProducerModule { @Produces ListenableFuture<UserData> provideUserData() { // ~ なんかめっちゃ時間がかかる処理 ~ return Futures.immediateFuture(new UserData("userName")); } @Produces String userName(UserData userData) { return userData.name; } }
実は Module がもう一つ必要で、それが以下に示す Executor
を返す Module です。
@Module public class ExecutorModule { @Provides @Production static Executor executor() { return Executors.newCachedThreadPool(); } }
この Executor は、非同期で依存を解決する Executor を提供するものです。
次に Component ですが、 @ProductionComponent
を使います。
@ProductionComponent(modules = {ExecutorModule.class, AppProducerModule.class}) public interface AppProducerComponent { ListenableFuture<String> userName(); }
ExecutorModule.class
はここで指定しています。
返り値が ListenableFuture<UserData>
となっている点に注意してください。
注入して欲しいクラス、例えば Activity は、この Component のメソッドからインスタンスを受け取ります。
public class MainActivity extends AppCompatActivity { ... ListenableFuture<String> userDataListenableFuture = DaggerAppProducerComponent.create().userName(); Futures.addCallback(userDataListenableFuture, new FutureCallback<String>() { @Override public void onSuccess(String result) { Log.e("name", result); } @Override public void onFailure(Throwable t) { } }); ... }
これを実行した時、 AppProducerModule
では順番にメソッドが呼ばれ、依存解決を試みます。
@ProducerModule final class AppProducerModule { @Produces ListenableFuture<UserData> provideUserData() { // 1. まずはじめにここが呼ばれる。 return Futures.immediateFuture(new UserData("userName")); } @Produces String userName(UserData userData) { // 1 で解決されるインスタンスが必要 // 2. 1が終わったらここが呼ばれる。 return userData.name; } }
Module は includes
を使うことによって、他の Module と関係をもつことも出来ます。
@ProducerModule(includes = UserDataRequestModule.class) final class AppProducerModule { @Produces ListenableFuture<UserData> provideUserData(APIClient client) { return Futures.immediateFuture(client.get())); } @Produces String userName(UserData userData) { return userData.name; } }
UserDataRequestModule.class
が APIClient
をくれる、というイメージです。
生成されたコードでExecutorはどう使われるか
処理はすべて非同期で、 Executor
を用いて行われます。先ほど ExecutorModule
クラスで Executor
を返すメソッドを定義しましたが、この Executor
は一体どのように扱われているのでしょうか。
生成されたコードを見てみると、その答えがわかります。
public final class DaggerAppComponent implements AppComponent { private Provider<Executor> executorProvider; ... this.executorProvider = DoubleCheck.provider( AppComponent_ProductionExecutorModule_ExecutorFactory.create( ExecutorModule_ExecutorFactory.create())); ...
Executor
は Dagger によって生成されたコードの中で、 Provider<Executor>
として保持されます。
この Provider はそれぞれの Factory クラス内でコンストラクタ経由で渡され、
public final class DaggerAppComponent implements AppComponent { ... this.getUserDataProducer = new AppProducerModule_GetUserDataFactory( builder.appProducerModule, executorProvider, monitorProvider); ...
Factory クラスが compute()
メソッドによって処理を始めた時、 Futures の transformAsync()
メソッドで非同期処理を行うときに get()
メソッドを呼ぶことで Executor
を得ています。
public final class AppProducerModule_GetUserDataFactory extends AbstractProducer<UserData> { ... @Override protected ListenableFuture<UserData> compute(final ProducerMonitor monitor) { return Futures.transformAsync( Futures.<Void>immediateFuture(null), new AsyncFunction<Void, UserData>() { @Override public ListenableFuture<UserData> apply(Void ignoredVoidArg) { monitor.methodStarting(); try { return AppProducerModule_GetUserDataFactory.this.module.getUserData(); } finally { monitor.methodFinished(); } } }, executorProvider.get()); }
補助的な機能
Dagger2には、様々な非同期の依存解決を実現する機能が用意されています。
Producer
先程も登場した Provider<T>
と似た仕組みが Producers には用意されています。それが Producer<T>
です。
まずは Module ですが、いわゆる出口であるメソッド以外で特に変わることはありません。
@Produces @Normal ListenableFuture<UserData> provideNUserDate() { return Futures.immediateFuture(new UserData("normal")); } @Produces @Special ListenableFuture<UserData> provideSUserDate() { return Futures.immediateFuture(new UserData("special")); } @Produces ListenableFuture<UserData> provideUserData(@Normal Producer<UserData> nProducer, @Special Producer<UserData> sProducer) { return sProducer.get(); }
受け取り側は、インスタンスそのものを受け取る or Producer を受け取るかを決定する権利を持っています。この点は Provider<T>
の性質と全く同じです。
これによって provideUserData(...)
は、何かしらの処理によって依存解決処理そのものを決定できる権利を持つのです。
例えば、 Dagger2 のドキュメントの例には Flag を見て処理を分けています。
Component も、そのメソッドを使う注入先の記述も何も変わりません。手を入れるのは Module だけです。
Produced
以下の Module のコードを見てください。確実に例外が発生するコードです。
@ProducerModule final class AppProducerModule { @Produces ListenableFuture<UserData> getUserData() { // ~ なんかめっちゃ時間がかかる処理 ~ throw new IllegalStateException(); // わざと落とす //return Futures.immediateFuture(new UserData("userName")); } @Produces String userName(UserData userData) { return userData.name; } }
この時、Dagger は依存の解決を諦め、スキップを行います。これは Module 内で何か都合の悪い予期せぬ自体が起きた時、例外が起きてクラッシュするのを防ぐためであると思われます。
ただ、例外を自らの手でハンドリングしたいことも有ります。その場合は Produced<T>
を使うことで実現できます。
@ProducerModule final class AppProducerModule { @Produces ListenableFuture<UserData> getUserData() { // ~ なんかめっちゃ時間がかかる処理 ~ throw new IllegalStateException(); // わざと落とす //return Futures.immediateFuture(new UserData("userName")); } @Produces String userName(Produced<UserData> userData) { try { return userData.get().name; } catch (ExecutionException e) { e.printStackTrace(); return "何かが起きた"; } } }
スタックトレースが吐かれ、"何かが起きた"が返されます。
@IntoSet
複数のインスタンスを Set<T>
に保持し、依存を解決する仕組みがあります。それが @IntoSet
です。
Module のメソッドに付与することで機能します。以下の例を御覧ください。
public class AppProducerMultiBindingsModule { @Produces @IntoSet ListenableFuture<UserData> provideNUserDate() { return Futures.immediateFuture(new UserData("normal")); } @Produces @IntoSet ListenableFuture<UserData> provideSUserDate() { return Futures.immediateFuture(new UserData("special")); } @Produces @IntoSet UserData provideDogUserDate() { return new UserData("dog"); } }
Component はこのようにします。
@ProductionComponent(modules = {ExecutorModule.class, AppProducerMultiBindingsModule.class}) public interface AppProducerMultiBindingsComponent { ListenableFuture<Set<UserData>> getSet(); }
getSet()
が呼ばれた時、関連するすべての依存の解決が試みられます。例えば上の Module の例では、すべての provide~ メソッドが、getSet()
された瞬間に呼ばれるということです。インスタンスが生成され次第、Set<t>
の中に追加されていきます。
ListenableFuture<Set<UserData>> set = ((App) getApplication()).getAppProducerMultiBindingsComponent().getSet(); Futures.addCallback(set, new FutureCallback<Set<UserData>>() { @Override public void onSuccess(Set<UserData> result) { Log.e("success", String.valueOf(result.size())); // 3 } @Override public void onFailure(Throwable t) { Log.e("error", t.toString()); } });
すべての処理が終わり次第、 onSuccess()
が呼ばれる、ということになります。
Module 内でこの Set<T>
を利用して新たな値を返す技もあります。以下の例では(意味不明ですが)すべての UserData
の名前を合体させて、新しい UserData
を返しています。
@ProducerModule public class AppProducerMultiBindingsModule { @Produces @IntoSet ListenableFuture<UserData> provideNUserDate() { return Futures.immediateFuture(new UserData("normal")); } @Produces @IntoSet ListenableFuture<UserData> provideSUserDate() { return Futures.immediateFuture(new UserData("special")); } @Produces @IntoSet UserData provideDUserDate() { return new UserData("dog"); } @Produces UserData collect(Set<UserData> data) { Iterator<UserData> i = data.iterator(); UserData normal = i.next(); UserData special = i.next(); UserData dog = i.next(); return new UserData(normal.name + " and " + special.name + " and " + dog.name); } }
@ElementsIntoSet
Set<T>
は Dagger で生成されたコードによって作られますが、 @ElementsIntoSet
を使えば、複数の値を Set<T>
に入れておくメソッドを自分で書くことも出来ます。
@ProducerModule public class AppProducerMultiBindingsModule { @Produces @IntoSet ListenableFuture<UserData> provideNUserDate() { return Futures.immediateFuture(new UserData("normal")); } @Produces @IntoSet ListenableFuture<UserData> provideSUserDate() { return Futures.immediateFuture(new UserData("special")); } @Produces @IntoSet UserData provideDUserDate() { return new UserData("dog"); } @Produces @ElementsIntoSet ListenableFuture<Set<UserData>> provideSet() { return Futures.<Set<UserData>>immediateFuture(ImmutableSet.of(new UserData("foo1"))); } @Produces UserData collect(Set<UserData> data) { Iterator<UserData> i = data.iterator(); UserData normal = i.next(); UserData special = i.next(); UserData dog = i.next(); UserData foo1 = i.next(); // @ElementsIntoSet return new UserData(normal.name + " and " + special.name + " and " + dog.name); } }
@IntoMap
Set<T>
だけでなく Map<K, V>
で扱うことも出来ます。
@MapKey @interface Type { String value(); } @ProducerModule public class AppModule { @Produces @IntoMap @Type("cat") static ListenableFuture<UserData> cat() { return Futures.immediateFuture(new UserData("cat")); // このコードでは呼ばれない } @Produces @IntoMap @Type("dog") static ListenableFuture<UserData> dog() { return Futures.immediateFuture(new UserData("dog")); // このコードでは呼ばれない } @Produces ListenableFuture<Integer> size(Map<String, Producer<UserData>> map) { return Futures.immediateFuture(map.size()); // 2 } }
この例はすべての UserData の合計値を出しているものですが、UserData そのものを受け取っていません。受け取っているのは Producer<UserData>
であり、単に map の size を得ているだけです。そのため、 cat()
メソッドと dog()
メソッドを動かすこと無くサイズを得ています。
まとめ
Producers を使えば、記事冒頭に書いたような要件をすべてクリアできるはずです。ちなみに Module 内のメソッドが一つ一つ非同期に流れていくさまはすこし RxJava ようではありますが、この仕組みはあくまでも非同期な依存注入を実現するためのものです。 RxJava と競合する仕組みでは勿論ありませんし、むしろ組み合わせて使うことでより利便性が向上するものであると理解しています。
効果的に使うことで大きな利益をもたらすと思われますが、まだ実際にプロダクトに導入もしておらず、僕自身も未だ深くは理解できていません。
もう少し遊んでみて、確実に効果の出せる箇所がありそうなら使っていきたいなと思っています。
より深い理解を望む場合は、Dagger2 の公式ドキュメントを御覧ください。 google.github.io