読者です 読者をやめる 読者になる 読者になる

┗┐<(՞ਊ՞)>┌┛

プログラミングとミニクーパーS(F56)とバイク(セロー)と酒のブログ

Dagger2のProducers 基本的な使い方

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'

build.gradle で producers を追加する

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;
    }
}

時間がかかる処理があると仮定した AppProducerModule.java

実は Module がもう一つ必要で、それが以下に示す Executor を返す Module です。

@Module
public class ExecutorModule {
    @Provides
    @Production
    static Executor executor() {
        return Executors.newCachedThreadPool();
    }
}

ExecutorModule.java

この Executor は、非同期で依存を解決する Executor を提供するものです。

次に Component ですが、 @ProductionComponent を使います。

@ProductionComponent(modules = {ExecutorModule.class, AppProducerModule.class})
    public interface AppProducerComponent {
        ListenableFuture<String> userName();
    }

非同期依存注入で利用する AppProducerComponent.java

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) {

            }
        });
    ...
}

結果を受け取る MainActivity.java

これを実行した時、 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;
    }

}

AppProducerModule.java 内の処理の流れ

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 を includes する

UserDataRequestModule.classAPIClient をくれる、というイメージです。

生成されたコードで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()));
  ...

生成された DaggerAppComponent.java

Executor は Dagger によって生成されたコードの中で、 Provider<Executor> として保持されます。 この Provider はそれぞれの Factory クラス内でコンストラクタ経由で渡され、

public final class DaggerAppComponent implements AppComponent {
...
    this.getUserDataProducer =
        new AppProducerModule_GetUserDataFactory(
            builder.appProducerModule, executorProvider, monitorProvider);
...

UserData を 供給する Producer を作る AppProducerModule_GetUserDataFactory() で渡されている

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());
  }

Dagger によって生成された AppProducerModule_GetUserDataFactory.java

補助的な機能

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();
    }

Producer を受け取る provideUserData() が定義されている AppProducerModule.java

受け取り側は、インスタンスそのものを受け取る 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;
    }
}

例外が起こって UserData を return できない AppProducerModule.java

この時、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 "何かが起きた";
        }
    }
}

必ず "何かが起きた" を返す AppProducerModule.java

スタックトレースが吐かれ、"何かが起きた"が返されます。

@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");
    }
}

@IntoSet を付与したメソッドを定義した AppProducerMultiBindingsModule.java

Component はこのようにします。

@ProductionComponent(modules = {ExecutorModule.class, AppProducerMultiBindingsModule.class})
public interface AppProducerMultiBindingsComponent {
    ListenableFuture<Set<UserData>> getSet();
}

ListenableFuture<Set> を返すメソッドを定義した AppProducerMultiBindingsComponent.java

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());
            }
        });

結果を Log.e(...) で出力する MainActivity.java

すべての処理が終わり次第、 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);
    }
}

collect() ですべての UserData.name を 足しあわせて返す AppProducerMultiBindingsModule.java

@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);
    }
}

@ElementsIntoSet を付与した provideSet() を定義した AppProducerMultiBindingsModule.java

@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
    }
}

@IntoMap を使い、生成されたMapを使って Integer を返す AppModule.java

この例はすべての UserData の合計値を出しているものですが、UserData そのものを受け取っていません。受け取っているのは Producer<UserData> であり、単に map の size を得ているだけです。そのため、 cat() メソッドと dog() メソッドを動かすこと無くサイズを得ています。

まとめ

Producers を使えば、記事冒頭に書いたような要件をすべてクリアできるはずです。ちなみに Module 内のメソッドが一つ一つ非同期に流れていくさまはすこし RxJava ようではありますが、この仕組みはあくまでも非同期な依存注入を実現するためのものです。 RxJava と競合する仕組みでは勿論ありませんし、むしろ組み合わせて使うことでより利便性が向上するものであると理解しています。

効果的に使うことで大きな利益をもたらすと思われますが、まだ実際にプロダクトに導入もしておらず、僕自身も未だ深くは理解できていません。

もう少し遊んでみて、確実に効果の出せる箇所がありそうなら使っていきたいなと思っています。

より深い理解を望む場合は、Dagger2 の公式ドキュメントを御覧ください。 google.github.io

広告を非表示にする