Dagger Hiltのコード生成の基本を解く
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
アノテーションをつけるのみで様子を見てみます。
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してくれそうですね。
...
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を生成しているようです。ComponentSupplier
はget
メソッドを持つただのインタフェースでした。
生成しているDaggerHiltStudyApp_HiltComponents_SingletonC
はアノテーションにより生成されたクラスです。これまでDagger2を使ったことのある人は見覚えのある記述ではないでしょうか。これがアプリケーションレベルのコンポーネントのようです。
ApplicationContextModule
なんてのもこのコンポーネントに指定しているようです。これはHilt内部クラスで、どうやらApplicatinContext
をオブジェクトグラフに登録しているようですね。
その後、Application
のonCreate
をオーバーライドし、HiltStudyAppがコンポーネントにアクセスできるようinjectHiltStudyApp
を呼び出しています。
...
@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
が実装しています。
generatedComponent
はHilt_HiltStudyApp
が実装しています。ApplicationComponentManager
内部のコンポーネント、つまりDaggerHiltStudyApp_HiltComponents_SingletonC
を返す実装になっています。
HiltStudyApp_HiltDaggerHiltStudyApp_HiltComponents_SingletonCComponents
次にこれです。前述の通り、生成されたHilt_HiltStudyApp
の持つApplicationComponentManager
が内部で持っているコンポーネントになります。
このクラスは内部に自身のビルダークラス、ViewModelレベルのコンポーネントとそのビルダークラス、サービスレベルのコンポーネントとそのビルダークラスが定義されていました。
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
クラスのみです。
...
@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
の例です。
@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
アノテーションをアプリケーションで定義しただけの段階では以下のようなコードが生成されました。
/**
* 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
を使った登録。
class Hoge @Inject constructor()
こちらは@Inject
を使った登録に@Singleton
スコープを付与。
@Singleton
class Fuga @Inject constructor()
こちらはインタフェースを定義します。
interface Piyo
class PiyoImpl : Piyo
そして、モジュールを使ってSingletonComponent
に登録。
@Module
@InstallIn(SingletonComponent::class)
object HiltStudyAppModule {
@Provides
fun providePiyo(): Piyo {
return PiyoImpl()
}
}
さらにこれらをHiltStudyApp
にメンバー変数としてインジェクトしてみます。
@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
は以下のように定義されます。
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までは、このメソッドはからの実装でしたが、ここに以下のようにコードが追加されます。
...
@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()
メソッドで取得していることに気づきます。
このメソッドは別途、以下のように追加されていました。
...
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行のみ追加されていました。
...
@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
につけるのみで様子を見てみます。
@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
と同じパターンです。
ActivityComponentManager
はApplicationComponentManager
と違い、generatedComponent()
メソッドで取得するコンポーネントがシングルトンになっているという点です。前述したように、Hiltでは1つのアクティビティコンポーネントを使用してすべてのアクティビティを注入するからです。
また、Hilt_MainActivity
にはViewModelProvider.Factory
を取得するメソッドも定義されていました。
MainActivity_GeneratedInjector
アクティビティコンポーネントにMainActivity
がアクセスできるようにinjectMainActivity()
メソッドがインタフェースとして定義しています。
ここもアプリケーションのときと同じ流れですね。
DaggerHiltStudyApp_HiltComponents_SingletonCの変更
アクティビティコンポーネントにMainActivity
がアクセスできるようにするメソッドが追加されたのみでした。
+ @Override
+ public void injectMainActivity(MainActivity mainActivity) {
+ }
HiltStudyApp_HiltComponentsの変更
アクティビティコンポーネントがMainActivity_GeneratedInjector
を実装するような追加がされたのみでした。
@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
を使った登録。
class HogeHoge @Inject constructor(
private val hoge: Hoge,
)
こちらは@Inject
を使った登録に@ActivityScoped
スコープを付与。
@ActivityScoped
class FugaFuga @Inject constructor(
private val fuga: Fuga,
)
こちらはインタフェースを定義します。
interface PiyoPiyo
class PiyoPiyoImpl @Inject constructor(
private val piyo: Piyo,
) : PiyoPiyo
そして、モジュールを使ってActivityComponent
に登録。
@Module
@InstallIn(ActivityComponent::class)
object MainActivityModule {
@Provides
fun providePiyoPiyo(
piyo: Piyo,
): PiyoPiyo {
return PiyoPiyoImpl(piyo)
}
}
さらにこれらをMainActivity
にメンバー変数としてインジェクトしてみます。
ついでにHoge
やFuga
もインジェクトしてみましょう。
@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