🕵️‍♂️

Dagger Hiltのコード生成の基本を解く

2021/01/23に公開

AndroidにおけるDIは、Dagger Hiltの登場により、より一層ボイラープレートが減り定義しやすくなりました。 開発者としては非常に嬉しく、個人・プロダクトでも導入しています。
この最高の黒魔術でどのようにしてDIを行っているのか、それを少しでも知る旅に出るのでした。

対象読者

  • Daggerをなんとなく理解している
  • Hiltを使ったことがある/調べたことがある
  • Dagger Hiltが何しているか気になる

前提

環境はこちら。

  • Android Studio 4.2 Beta 3
  • Dagger Hilt 2.31.2-alpha

今回はAndroidのアプリケーションクラスを継承したクラスをHiltStudyAppとします。

ソースコードはGitHubにあげているのであわせて御覧ください。それぞれのステップごとにブランチを切ってあります。

Step1. Hiltをアプリケーションに適用する。

導入方法は割愛です。

まずは@HiltAndroidAppアノテーションをつけるのみで様子を見てみます。

HiltStudyApp.kt
package com.rmakiyama.daggerhiltstudy

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltStudyApp : Application()

こちらをビルドすると、以下のファイルが自動生成されます。

  • Hilt_HiltStudyApp
  • DaggerHiltStudyApp_HiltComponents_SingletonC
  • HiltStudyApp_GeneratedInjector
  • HiltStudyApp_HiltComponents
  • com_rmakiyama_daggerhiltstudy_HiltStudyApp_GeneratedInjectorModuleDeps

それぞれについて見ていきましょう。

Hilt_HiltStudyApp

どうやらHilt_${application_name}なクラスが生成されるようです。AndroidのApplicationクラスを継承しています。

クラスを見てみるとHilt_HiltStudyAppはHiltライブラリの持つクラスであるApplicationComponentManagerを内部で保持していました。見るからにApllicaionのComponentをManagerしてくれそうですね。

Hilt_HiltStudyApp.java
  ...
  private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
    @Override
    public Object get() {
      return DaggerHiltStudyApp_HiltComponents_SingletonC.builder()
          .applicationContextModule(new ApplicationContextModule(Hilt_HiltStudyApp.this))
          .build();
    }
  });
  ...

ComponentSupplierを使ってComponentを生成しているようです。ComponentSuppliergetメソッドを持つただのインタフェースでした。
生成しているDaggerHiltStudyApp_HiltComponents_SingletonCはアノテーションにより生成されたクラスです。これまでDagger2を使ったことのある人は見覚えのある記述ではないでしょうか。これがアプリケーションレベルのコンポーネントのようです。
ApplicationContextModuleなんてのもこのコンポーネントに指定しているようです。これはHilt内部クラスで、どうやらApplicatinContextをオブジェクトグラフに登録しているようですね。

その後、ApplicationonCreateをオーバーライドし、HiltStudyAppがコンポーネントにアクセスできるようinjectHiltStudyAppを呼び出しています。

Hilt_HiltStudyApp.java
  ...
  @CallSuper
  @Override
  public void onCreate() {
    // This is a known unsafe cast, but is safe in the only correct use case:
    // HiltStudyApp extends Hilt_HiltStudyApp
    ((HiltStudyApp_GeneratedInjector) generatedComponent()).injectHiltStudyApp(UnsafeCasts.<HiltStudyApp>unsafeCast(this));
    super.onCreate();
  }
  ...

ここで出たHiltStudyApp_GeneratedInjectorは、アノテーションにより生成されたインタフェースです。DaggerHiltStudyApp_HiltComponents_SingletonCが実装しています。

generatedComponentHilt_HiltStudyAppが実装しています。ApplicationComponentManager内部のコンポーネント、つまりDaggerHiltStudyApp_HiltComponents_SingletonCを返す実装になっています。

HiltStudyApp_HiltDaggerHiltStudyApp_HiltComponents_SingletonCComponents

次にこれです。前述の通り、生成されたHilt_HiltStudyAppの持つApplicationComponentManagerが内部で持っているコンポーネントになります。

このクラスは内部に自身のビルダークラス、ViewModelレベルのコンポーネントとそのビルダークラス、サービスレベルのコンポーネントとそのビルダークラスが定義されていました。

DaggerHiltStudyApp_HiltComponents_SingletonC.java
public final class DaggerHiltStudyApp_HiltComponents_SingletonC extends HiltStudyApp_HiltComponents.SingletonC {
  ...
  public static final class Builder { ... }
  private final class ActivityRetainedCBuilder ...
  private final class ActivityRetainedCImpl ...
  private final class ServiceCBuilder ...
  private final class ServiceCImpl ...
}

さらに、ViewModelレベルのコンポーネントクラスの内部では、アクティビティレベルのコンポーネントとそのビルダークラスが定義されています。さらに…といった具合に、Hiltの定義したコンポーネント階層に沿ってコードが生成されていました。おもしろい。

各レベルのコンポーネントは、HiltStudyApp_HiltComponentsに定義された抽象クラスを継承しているようです。見やすいので後ほどかんたんに説明します。

HiltStudyApp_GeneratedInjector

前述したように、DaggerHiltStudyApp_HiltComponents_SingletonCにより実装されているインタフェースです。injectHiltStudyAppのみ定義されています。

これをアプリケーションクラスが呼び出すことにより、アプリケーションレベルのコンポーネントにHiltStudyAppがアクセスできるようになります。(自動生成でHilt_HiltStudyAppがやっている。)

HiltStudyApp_HiltComponents

アプリケーションにおけるHiltのコンポーネント定義がこちらにまとまっています。ここでコンポーネント階層の標準化がなされているようです。

@Componentアノテーションがつくのは唯一HiltStudyApp_HiltComponents.SingletonCクラスのみです。

HiltStudyApp_HiltComponents.java
  ...
  @Component(
      modules = {
          ApplicationContextModule.class,
          ActivityRetainedCBuilderModule.class,
          ServiceCBuilderModule.class
      }
  )
  @Singleton
  public abstract static class SingletonC implements HiltStudyApp_GeneratedInjector,
      HiltWrapper_ActivityRetainedComponentManager_ActivityRetainedComponentBuilderEntryPoint,
      ServiceComponentManager.ServiceComponentBuilderEntryPoint,
      SingletonComponent,
      GeneratedComponent {
  }
  ...

SingletonComponentのデフォルトのバインディングであるアプリケーションを提供するApplicationContextModuleの他、コンポーネント階層に従って、ActivityRetainedCBuilderModule/ServiceCBuilderModuleがコンポーネントに指定されています。

その他のコンポーネントはそれぞれ@Subcomponentアノテーションがついたサブコンポーネントとして定義されています。そして、デフォルトのバインディングを提供するモジュールと、コンポーネント階層に従ったモジュールが指定されています。
以下はActivityComponentの例です。

HiltStudyApp_HiltComponents.java
  @Subcomponent(
      modules = {
          FragmentCBuilderModule.class,
          ViewCBuilderModule.class,
          HiltWrapper_ActivityModule.class,
          HiltWrapper_DefaultViewModelFactories_ActivityModule.class
      }
  )
  @ActivityScoped
  public abstract static class ActivityC implements ActivityComponent,
      DefaultViewModelFactories.ActivityEntryPoint,
      FragmentComponentManager.FragmentComponentBuilderEntryPoint,
      ViewComponentManager.ViewComponentBuilderEntryPoint,
      GeneratedComponent {
    @Subcomponent.Builder
    abstract interface Builder extends ActivityComponentBuilder {
    }
  }

こうして、Hiltではアクティビティごとに1つではなく、唯一1つのアクティビティコンポーネントを持つことがわかります。

com_rmakiyama_daggerhiltstudy_HiltStudyApp_GeneratedInjectorModuleDeps

@HiltAndroidAppアノテーションをアプリケーションで定義しただけの段階では以下のようなコードが生成されました。

com_rmakiyama_daggerhiltstudy_HiltStudyApp_GeneratedInjectorModuleDeps.java
/**
 * Generated class to pass information through multiple javac runs.
 */
@AggregatedDeps(
    components = "dagger.hilt.components.SingletonComponent",
    entryPoints = "com.rmakiyama.daggerhiltstudy.HiltStudyApp_GeneratedInjector"
)
class com_rmakiyama_daggerhiltstudy_HiltStudyApp_GeneratedInjectorModuleDeps {
}

コメントにあるように、コンパイラに生成クラスのパスを知らせるためのクラスかなという理解までしかできませんでした…。一旦保留。

Step2. オブジェクトグラフにバインディングを登録する。

次に、オブジェクトグラフにバインディングを登録してみます。

こちらは@Injectを使った登録。

Hoge.kt
class Hoge @Inject constructor()

こちらは@Injectを使った登録に@Singletonスコープを付与。

Fuga.kt
@Singleton
class Fuga @Inject constructor()

こちらはインタフェースを定義します。

Piyo.kt
interface Piyo

class PiyoImpl : Piyo

そして、モジュールを使ってSingletonComponentに登録。

HiltStudyAppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object HiltStudyAppModule {

    @Provides
    fun providePiyo(): Piyo {
        return PiyoImpl()
    }
}

さらにこれらをHiltStudyAppにメンバー変数としてインジェクトしてみます。

HiltStudyApp.kt
@HiltAndroidApp
class HiltStudyApp : Application() {
    @Inject lateinit var hoge: Hoge
    @Inject lateinit var fuga: Fuga
    @Inject lateinit var piyo: Piyo
}

ここでビルドすると以下のクラスが生成されました。

  • Hoge_Factory
  • Fuga_Factory
  • HiltStudyAppModule_ProvidePiyoFactory
  • HiltStudyApp_MembersInjector

また、以下のクラスにコードが追加されました。

  • DaggerHiltStudyApp_HiltComponents_SingletonC
  • HiltStudyApp_HiltComponents

それぞれのFactoryクラス

オブジェクトグラフに登録したクラスは、それぞれファクトリーが生成されました。
それぞれのファクトリークラスは、DaggerのProvider<T>getを実装していて、対象のクラスのインスタンスを生成しています。

Hoge_Factory/Fuga_Factoryは、引数を取るものではないため単純にデフォルトのコンストラクタを呼ぶことで生成されます。

HiltStudyAppModule_ProvidePiyoFactoryは、HiltStudyAppModule@Providesをつけて定義した関数を用いてインスタンスを生成しています。

HiltStudyApp_MembersInjector

名前の通り、それぞれのインスタンスをメンバー変数としてインジェクトするためのクラスです。
Hoge/Fuga/Piyoについてそれぞれプロバイダを保持しています。
これらのプロバイダは、create関数によってDagger内部で生成されるようです。

また、それぞれを実際にメンバー変数としてインジェクトするためのメソッドとして、injectHoge/injectFuga/injectPiyoが定義されています。
たとえば、injectHogeは以下のように定義されます。

HiltStudyApp_MembersInjector.java
  public static void injectHoge(HiltStudyApp instance, Hoge hoge) {
    instance.hoge = hoge;
  }

引数として、インジェクトする先のインスタンスと、インジェクトするクラスのインスタンスが渡されています。
そしてこれらは、DaggerHiltStudyApp_HiltComponents_SingletonCから呼び出されていました。

DaggerHiltStudyApp_HiltComponents_SingletonCの変更

Step1で説明したように、Hilt_HiltStudyAppはアプリケーションレベルのコンポーネントに対して(つまりDaggerHiltStudyApp_HiltComponents_SingletonCの変更に対して)、injectHiltStudyApp()メソッドで、注入を要求するインスタンス(HiltStudyApp)を渡しています。

Step1までは、このメソッドはからの実装でしたが、ここに以下のようにコードが追加されます。

DaggerHiltStudyApp_HiltComponents_SingletonC.java
  ...
  @Override
  public void injectHiltStudyApp(HiltStudyApp hiltStudyApp) {
    injectHiltStudyApp2(hiltStudyApp);
  }

  private HiltStudyApp injectHiltStudyApp2(HiltStudyApp instance) {
    HiltStudyApp_MembersInjector.injectHoge(instance, new Hoge());
    HiltStudyApp_MembersInjector.injectFuga(instance, fuga());
    HiltStudyApp_MembersInjector.injectPiyo(instance, HiltStudyAppModule_ProvidePiyoFactory.providePiyo());
    return instance;
  }
  ...

injectHiltStudyApp2()メソッドが生成され、HiltStudyApp_MembersInjectorの各メソッドを呼び出し、メンバー変数にインスタンスを注入していることがわかります。

ここで、@Singletonスコープをつけて定義したFugaクラスのインスタンスはfuga()メソッドで取得していることに気づきます。
このメソッドは別途、以下のように追加されていました。

DaggerHiltStudyApp_HiltComponents_SingletonC.java
  ...
  private volatile Object fuga = new MemoizedSentinel();
  ...
  private Fuga fuga() {
    Object local = fuga;
    if (local instanceof MemoizedSentinel) {
      synchronized (local) {
        local = fuga;
        if (local instanceof MemoizedSentinel) {
          local = new Fuga();
          fuga = DoubleCheck.reentrantCheck(fuga, local);
        }
      }
    }
    return (Fuga) local;
  }
  ...

細かい説明は省きますが、一度生成したインスタンスを使いまわしていることがわかります。指定したスコープ通り、シングルトンなインスタンスが注入されていますね。

HiltStudyApp_HiltComponentsの変更

HiltStudyApp_HiltComponentsについては1行のみ追加されていました。

HiltStudyApp_HiltComponents.java
  ...
  @Component(
      modules = {
          ApplicationContextModule.class,
+         HiltStudyAppModule.class,
          ActivityRetainedCBuilderModule.class,
          ServiceCBuilderModule.class
      }
  )
  @Singleton
  public abstract static class SingletonC implements HiltStudyApp_GeneratedInjector,
      HiltWrapper_ActivityRetainedComponentManager_ActivityRetainedComponentBuilderEntryPoint,
      ServiceComponentManager.ServiceComponentBuilderEntryPoint,
      SingletonComponent,
      GeneratedComponent {
  }
  ...

Dagger2を使ったことがある人は、モジュールを追加するたびにそれをコンポーネントへ指定していたことを思い出すでしょう。Hiltではこのようにしてこれを自動でやってくれます。便利。

モジュールを追加して@InstallInアノテーションをつけるだけで指定のコンポーネントのオブジェクトグラフにバインディング登録される謎がこれで解けました。

その他

com_rmakiyama_daggerhiltstudy_HiltStudyAppModuleModuleDepsクラスも生成されていましたが、割愛します。Step1同様、コンパイラのためのクラスでしょうか。

Step3. アクティビティにAndroidEntryPointアノテーションをつける。

まずは@AndroidEntryPointアノテーションをMainActivityにつけるのみで様子を見てみます。

MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

この状態でビルドすると、以下のファイルが自動生成されます。

  • Hilt_MainActivity
  • MainActivity_GeneratedInjector

そして、以下のクラスにコードが追加されました。

  • DaggerHiltStudyApp_HiltComponents_SingletonC
  • HiltStudyApp_HiltComponents

なにやら見覚えがありますね。それぞれを軽く見ていきます。

Hilt_MainActivity

Hilt_MainActivityはHiltライブラリの持つクラスであるActivityComponentManagerを内部で保持していました。
概ね実装はHilt_HiltStudyAppと同じパターンです。

ActivityComponentManagerApplicationComponentManagerと違い、generatedComponent()メソッドで取得するコンポーネントがシングルトンになっているという点です。前述したように、Hiltでは1つのアクティビティコンポーネントを使用してすべてのアクティビティを注入するからです。

また、Hilt_MainActivityにはViewModelProvider.Factoryを取得するメソッドも定義されていました。

MainActivity_GeneratedInjector

アクティビティコンポーネントにMainActivityがアクセスできるようにinjectMainActivity()メソッドがインタフェースとして定義しています。
ここもアプリケーションのときと同じ流れですね。

DaggerHiltStudyApp_HiltComponents_SingletonCの変更

アクティビティコンポーネントにMainActivityがアクセスできるようにするメソッドが追加されたのみでした。

DaggerHiltStudyApp_HiltComponents_SingletonC.java
+     @Override
+     public void injectMainActivity(MainActivity mainActivity) {
+     }

HiltStudyApp_HiltComponentsの変更

アクティビティコンポーネントがMainActivity_GeneratedInjectorを実装するような追加がされたのみでした。

HiltStudyApp_HiltComponents.java
  @Subcomponent(
      modules = {
          FragmentCBuilderModule.class,
          ViewCBuilderModule.class,
          HiltWrapper_ActivityModule.class,
          HiltWrapper_DefaultViewModelFactories_ActivityModule.class
      }
  )
  @ActivityScoped
+ public abstract static class ActivityC implements MainActivity_GeneratedInjector,
+     ActivityComponent,
      DefaultViewModelFactories.ActivityEntryPoint,
      FragmentComponentManager.FragmentComponentBuilderEntryPoint,
      ViewComponentManager.ViewComponentBuilderEntryPoint,
      GeneratedComponent {
    @Subcomponent.Builder
    abstract interface Builder extends ActivityComponentBuilder {
    }
  }

Step4. アクティビティにインスタンスを注入する。

さらにオブジェクトグラフにバインディングを登録してみます。

こちらは@Injectを使った登録。

HogeHoge.kt
class HogeHoge @Inject constructor(
    private val hoge: Hoge,
)

こちらは@Injectを使った登録に@ActivityScopedスコープを付与。

FugaFuga.kt
@ActivityScoped
class FugaFuga @Inject constructor(
    private val fuga: Fuga,
)

こちらはインタフェースを定義します。

PiyoPiyo.kt
interface PiyoPiyo

class PiyoPiyoImpl @Inject constructor(
    private val piyo: Piyo,
) : PiyoPiyo

そして、モジュールを使ってActivityComponentに登録。

MainActivityModule.kt
@Module
@InstallIn(ActivityComponent::class)
object MainActivityModule {

    @Provides
    fun providePiyoPiyo(
        piyo: Piyo,
    ): PiyoPiyo {
        return PiyoPiyoImpl(piyo)
    }
}

さらにこれらをMainActivityにメンバー変数としてインジェクトしてみます。
ついでにHogeFugaもインジェクトしてみましょう。

MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var hoge: Hoge
    @Inject lateinit var hogeHoge: HogeHoge
    @Inject lateinit var fuga: Fuga
    @Inject lateinit var fugaFuga: FugaFuga
    @Inject lateinit var piyo: Piyo
    @Inject lateinit var piyoPiyo: PiyoPiyo
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

ここでビルドすると、前回同様、追加したクラスのファクトリークラスとメンバーインジェクターが生成されました。
またDaggerHiltStudyApp_HiltComponents_SingletonC/HiltStudyApp_HiltComponentsにコードが追加されていました。

さて、ここまでくると、なんとなく頭の中でHiltの生成するコードが浮かんでくるのではないでしょうか?

ぜひ頭の中で思い浮かべて、ソースコードをご確認ください!

まとめ

かんたんなHiltの連携とメンバー変数のインジェクトをした際の、Hiltが生成するコードについて見ていきました。
どんなコードが生成されているかを理解すると気持ちいいですね。Hiltの気持ちを少しわかった気がします。エラー文とも仲良くなれる気がします。

参考

Discussion