ApplicationComponent 実装の変遷 - Dagger2

2022-03-16 追記: 新しいDagger記事は以下リンクから

👉 MVVM で Hilt のパターン化 💉  

--------

へん‐せん【変遷】
[名](スル)時の流れとともに移り変わること。「歌もまた時代につれて変遷する」

Dagger て分かりづらいです。

タイトルがすでに謎ですが、以下のような実装のことを指しています。

アプリケーションコンテキストをオブジェクトグラフに追加する

ApplicationComponent とアプリケーションコンテキストの設定

アプリケーションコンテキスト を依存先として公開する

これまで数年に渡って変化し続けてるそんな必須の実装項目です。

ネット上をただ検索するだけでは、古い記事に最新の実装記述が埋没しています。

Dagger 2.9 以前( - 2017/02/04)


@Component(modules = [ApplicationModule::class, ...])
interface ApplicationComponent {
  ...
}


@Module
class ApplicationModule(private val applicationContext: Context) {
  @Provides fun provideApplicationContext() = applicationContext
}

これは Kotlin 記述で簡略化できます。


@Module
class ApplicationModule(@get:Provides val applicationContext: Context)


DaggerApplicationComponent
  .builder()
  .applicationModule(ApplicationModule(applicationContext))
  .build()

2.9 (2017/02/04 - ) @BindsInstance

👉 Release Dagger 2.9 · google/dagger 

We create a module that receives the application context as a constructor argument, and we create a provide method that exposes it. This works great, but then we can't have static @Provides methods anymore. And besides that, this strategy is actually going against the docs that are pretty explicit when it comes to this:

@BindsInstance methods should be preferred to writing a @Module with constructor arguments and immediately providing those values.

👉 User's Guide 


@Component(modules = ...)
interface ApplicationComponent {
  @Component.Builder
  interface Builder {
    @BindsInstance 
    fun applicationContext(applicationContext: Context): Builder
    fun build(): ApplicationComponent
  }
  ...
}


DaggerApplicationComponent
  .builder()
  .applicationContext(applicationContext)
  .build()

2.22 (2019/04/03 - ) @Component.Factory

👉 Release Dagger 2.22 · google/dagger 


@Component(modules = ...)
interface ApplicationComponent {
  @Component.Factory
  interface Factory {
    fun create(@BindsInstance applicationContext: Context): ApplicationComponent
  }
  ...
}


DaggerApplicationComponent
  .factory()
  .create(applicationContext)

まとめ

最近のコード記述を理解するには、少しだけ

「成り立ちを遡ってみる」

と理解しやすいことが多いように思います。

👉 Releases · google/dagger 
👉 Dagger 2 on Android: the shiny new @Component.Factory 

2022-03-16 追記: 新しいDagger記事は以下リンクから

👉 MVVM で Hilt のパターン化 💉  


あなたは Android Architecture Component をどう思いますか?

ある人々のTwitter上の会話。ザクッと翻訳サービスを利用して眺めてみます。

Frankly, the expectation is that all applications of a considerable size already use some form of DI. Thus providing yet another way to pass dependencies via context is an overkill. All "non-DI" facilities are just gimmicks/workarounds for cases when there is no DI.

率直に言って、かなりの数のアプリケーションがすでにDIを使用している。したがって、コンテキストを介して依存関係を渡すためのさらに別の方法を提供するのはやり過ぎです。すべての「非DI機能」は、DIがない場合のギミック/回避策にすぎません。

On Android this sadly isn't the case. We seem to relish in bad architecture.

It's either "Why do I have to pass an executor/scheduler/dispatcher? So much boilerplate!" or "Why can't I test on the JVM? This library is poorly designed!"

Maybe KEEP-87 will save us from ourselves?

Androidでは、これは悲しいことではありません。悪いアーキテクチャーで大喜びしているようです。

「executor / scheduler / dispatcher を渡す必要があるのはなぜですか? かなりのボイラープレートです。」、「なぜJVMでテストできないのですか?このライブラリは適切に設計されていません。」

多分KEEP-87は私達を私達自身から救うのだろうか?

👉 [Kotlin] KEEP87 brings compiler-driven dependency injection without frameworks : androiddev 

On unrelated note. (disclaimer: I'm not an Android developer) I have a feeling that something is broken in testing or architecture approaches here. I've been writing huge (1M+ LOCs) UI apps for more than a decade and never had to use either DI or statics to make them testable.

無関係なメモについて。 (免責事項:私はAndroidの開発者ではありません)何かがここでテストやアーキテクチャのアプローチで壊れていると感じています。私は10年以上にわたって巨大な(1M + LOC)UIアプリを書いてきました。そしてそれらをテストするために DI や statics を使う必要は決してありませんでした。

Android never had architecture guidelines and the docs encouraged doing all the wrong things to make the tutorials easy. Basically equivalent to writing everything in main(). Even now almost all of the architecture offerings treat symptoms of this legacy and not the disease.

Androidはアーキテクチャのガイドラインがなかったので、チュートリアルを簡単にするためにすべての間違ったことをすることをドキュメントを奨めてきました。基本的にmain() ですべてを書くのと同じです。現在でも、ほとんどのアーキテクチャがこの遺産の症状を治療していて、疾患を治療していません。

👉 Roman Elizarov on Twitter: "@JakeWharton Frankly, the expectation is that all applications of a considerable size already use some form of DI. Thus providing yet another way to pass dependencies via context is an overkill. All "non-DI" facilities are just gimmicks/workarounds for cases when there is no DI." / Twitter 

みなさんはどう思っていますか?

Roman Elizarov
@relizarov
Team Lead @JetBrains, working on @Kotlin coroutines and libs, sports programming/ICPC, concurrency & algorithms, math/quantitative finance; formerly @Devexperts

👉 Roman Elizarov (@relizarov) / Twitter 

Jake Wharton
@JakeWharton
Opinions expressed here are my own, not those of my company. They made me write this because I complain about Inbox going away so much.

👉 Jake Wharton (@JakeWharton) / Twitter 


Dagger に馴染めない人のためのいくつかの原則

Keeping the Daggers Sharp ⚔️ – Square Corner Blog – Medium

Dagger2 は 素晴らしい Dependency Injection ライブラリですが, なかなか上手に使いこなせません.

分かりやすくするための考え方や実装方法をいくつか見てみましょう.

フィールドよりコンストラクタのインジェクションを使う

フィールドインジェクションは, finalでなく, privateでないフィールドに使います.


// BAD
class CardConverter {

  @Inject PublicKeyManager publicKeyManager;

  @Inject public CardConverter() {}

}

フィールドに @Inject を忘れると NullPointerException の原因となります.


// BAD
class CardConverter {

  @Inject PublicKeyManager publicKeyManager;
  Analytics analytics; // Oops, forgot to @Inject

  @Inject public CardConverter() {}

}

コンストラクタインジェクションはイミュータブルですので, 局所的な状態を持ちませんのでスレッドセーフにつながります.


// GOOD
class CardConverter {

  private final PublicKeyManager publicKeyManager;

  @Inject public CardConverter(PublicKeyManager publicKeyManager) {
    this.publicKeyManager = publicKeyManager;
  }

}

Kotlinでは, さらに簡素化してくれます.


class CardConverter

@Inject constructor(
  private val publicKeyManager: PublicKeyManager)

それでも, フィールドインジェクションを使いたい場合は以下のようになります.


public class MainActivity extends Activity {

  public interface Component {
    void inject(MainActivity activity);
  }

  @Inject ToastFactory toastFactory;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Component component = SquareApplication.component(this);
    component.inject(this);
  }

}

Singleton はあまり使う必要はない

ミュータブルな状態に対して多くのアクセスが必要な場合は Singleton は便利です.


// GOOD
@Singleton
public class BadgeCounter {

  public final Observable<Integer> badgeCount;

  @Inject public BadgeCounter(...) {
     badgeCount = ...
  }

}

しかし, 変化しない状態に対しては Singleton にする必要はありません.


// BAD, should not be a singleton!
@Singleton
class RealToastFactory implements ToastFactory {

  private final Application context;

  @Inject public RealToastFactory(Application context) {
    this.context = context;
  }

  @Override public Toast makeText(int resId, int duration) {
    return Toast.makeText(context, resId, duration);
  }

}

まれに, 作成のコストがかかるキャッシュインスタンスの走査に使うことがあります. そうすることで, 繰り返し作成して破棄することを避けることができます.

@Provides でなく @Inject を使う

@Provides はコンストラクタを複製すべきではありません.
関係する部分を一つにするとコードがわかりやすくなります.


@Module
class ToastModule {

  // BAD, remove this binding and add @Inject to RealToastFactory
  @Provides RealToastFactory realToastFactory(Application context) {
    return new RealToastFactory(context);
  }

}

このことは, 特に singleton においては重要です. そのクラスを読むときの重要な実装の内容一覧となります. 一部分をみればすべての内容が把握できます.


// GOOD, I have all the details I need in one place.
@Singleton
public class BadgeCounter {

  @Inject public BadgeCounter(...) {}

}

static @Provides を使う

@Provides は static にすることができます.


@Module
class ToastModule {

  @Provides
  static ToastFactory toastFactory(RealToastFactory factory) {
    return factory;
  }

}

モジュールインスタンスを作成する代わりに, 直接メソッドを実行することができます. このときそのメソッドの呼び出しはコンパイラによってインライン化されています.


@Generated
public final class DaggerAppComponent extends AppComponent {

  // ...
  @Override public ToastFactory toastFactory() {
    return ToastModule.toastFactory(realToastFactoryProvider.get())
  }

}

一つだけのメソッドを static にしてもあまり変化はないですが, すべてを static にすると, かなりのパフォーマンスが向上します.

また, モジュールを abstract にすると, static でない @provides メソッドが ひとつでもあるとコンパイルに失敗します.

@Provides よりも @Binds を使う

あるタイプを他にマッピングするときは @Provides でなく @Binds を使う.


@Module
abstract class ToastModule {

  @Binds
  abstract ToastFactory toastFactory(RealToastFactory factory);

}

このメソッドは abstract でなければなりません. @Generated コードは実装内容をそのまま使おうとします.


@Generated
public final class DaggerAppComponent extends AppComponent {

  // ...
  private DaggerAppComponent() {
    // ...
    this.toastFactoryProvider = (Provider) realToastFactoryProvider;
  }

  @Override public ToastFactory toastFactory() {
    return toastFactoryProvider.get();
  }

}

@Singleton の interface binding は避ける

Statefulness is an implementation detail

集中するアクセスがミュータブルな状態にアクセスする必要があるかは実装のみが知っていますので, 実装をインターフェースにバインドさせるとき, アノテーションをつけるべきではありません.


@Module
abstract class ToastModule {

  // BAD, remove @Singleton
  @Binds @Singleton

  abstract ToastFactory toastFactory(RealToastFactory factory);

}

error-prone を使おう

一般的な Dagger のエラーはこれを使うことで分かりやすく検出できます.

👉 MVVM で Hilt のパターン化 💉  hatena-bookmark