最初に
Dagger Hiltの利用でField Injectionはスッと理解できたのですが、Constructor Injectionは少し時間がかかりました。自分用にもメモとして残します。
Dagger Hiltの環境設定やプロバイダのスコープ範囲などの説明はここでは割愛して、シンプルにコンストラクタインジェクションがどのように記述されるのかだけをまとめてます。
多分一番シンプルなコード
わかりやすくするためにString型を提供するプロバイダのサンプルです。
@AndroidEntryPoint
class HiltStringActivity : AppCompatActivity() {
// StringImplのコンストラクタには引数があるのに何も渡していない
// コンストラクタの引数はインジェクトしろというアノテーション
@Inject
lateinit var myStringImpl: StringImpl
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println(myStringImpl.getMyString())
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクションされる側
// ───────────────────────────────────────────
class StringImpl
@Inject
constructor(
// StringImplクラスのコンストラクタ引数(これがインジェクトされる)
// String型へインジェクトするプロバイダがあるはず
// (というかプロバイダを作る)
private val myString_anyName: String
) {
fun getMyString(): String {
return myString_anyName
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクション提供側
// ───────────────────────────────────────────
@Module
@InstallIn(ActivityComponent::class)
object StringProvider_anyName {
@ActivityScoped
@Provides
// これがString型へインジェクトするプロバイダ
// object名も関数名もなんでも良い
// 大事なのは「String型」を提供していること
fun provideString_anyName(): String {
return "String from Provider"
}
}
上記の例だと困る点
String型を提供するプロバイダが2種類必要な場合も考えられますが、同一のスコープでString型を提供するプロバイダを2つ作るとコンパイルエラーになります。
(複数提供する方法はある。後述。)
また、実務でString型を提供するプロバイダを作ることは考えにくいです。
String型の提供からクラス型提供へ少しだけ拡張
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject
lateinit var myClassImpl: MyClassImpl
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println(myClassImpl.get())
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクションしたい型
// ───────────────────────────────────────────
class MyClass {
val myInt: Int = 1
val myStr: String = "from MyClass"
}
// ───────────────────────────────────────────
// コンストラクタインジェクションされる側
// ───────────────────────────────────────────
class MyClassImpl
@Inject
constructor(
// MyClass型へインジェクトするプロバイダがあるはず
private val myClass: MyClass
) {
fun get(): String {
return "${myClass.myInt} ${myClass.myStr}"
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクション提供側
// ───────────────────────────────────────────
@Module
@InstallIn(ActivityComponent::class)
object MyClassProvider_anyName {
@ActivityScoped
@Provides
// MyClass型を提供
fun provideMyClass_anyName(): MyClass {
return MyClass()
}
}
実務ではテスト用のMockを想定してインターフェースを継承したクラスをプロバイドすることがほとんどだと思いますが、ここでは単純さを優先して直接クラスをプロバイドしています。
同一の型を複数のプロバイダから提供する1(@Named)
@AndroidEntryPoint
class HiltStringActivity : AppCompatActivity() {
@Inject
lateinit var myStringImpl: StringImpl
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println(myStringImpl.getMyString())
}
// ───────────────────────────────────────────
// コンストラクタインジェクション提供側
// ───────────────────────────────────────────
@Module
@InstallIn(ActivityComponent::class)
object StringProvider_anyName {
// @Namedで好きな名前を付与
@Named("StringProvider1")
@ActivityScoped
@Provides
fun provideString_anyName(): String {
return "String from Provider"
}
// @Namedで好きな名前を付与
@Named("StringProvider2")
@ActivityScoped
@Provides
fun provideMyClass_anyName(
@ApplicationContext context: Context
): String {
return context.getString(R.string.app_name)
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクションされる側
// ───────────────────────────────────────────
class StringImpl
@Inject
constructor(
// インジェクションされる側でどのプロバイダを利用するか指定する
@Named("StringProvider2")
private val myString_anyName: String
) {
fun getMyString(): String {
return myString_anyName
}
}
}
同一の型を複数のプロバイダから提供する2(annotation classを定義)
@Namedタグは簡単ですが、アノテーション名を厳密にクラスとして定義する方法がこちらです。
@AndroidEntryPoint
class HiltStringActivity : AppCompatActivity() {
@Inject
lateinit var myStringImpl: StringImpl
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println(myStringImpl.getMyString())
}
// ───────────────────────────────────────────
// String型を提供するプロバイダが2個ある
// ───────────────────────────────────────────
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class StringProvider1
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class StringProvider2
// ───────────────────────────────────────────
// コンストラクタインジェクション提供側
// ───────────────────────────────────────────
@Module
@InstallIn(ActivityComponent::class)
object StringProvider_anyName {
// それぞれのプロバイダへ定義したアノテーション名を付与
@StringProvider1
@ActivityScoped
@Provides
fun provideString_anyName(): String {
return "String from Provider"
}
// それぞれのプロバイダへ定義したアノテーション名を付与
@StringProvider2
@ActivityScoped
@Provides
fun provideMyClass_anyName(
@ApplicationContext context: Context
): String {
return context.getString(R.string.app_name)
}
}
// ───────────────────────────────────────────
// コンストラクタインジェクションされる側
// ───────────────────────────────────────────
class StringImpl
@Inject
constructor(
// Stringを提供する2つのプロバイダから@StringProvider2を使えというアノテーション
@StringProvider2 private val myString_anyName: String
) {
fun getMyString(): String {
return myString_anyName
}
}
}
参考にしました
DI (Dependency Injection) の基本
(https://qiita.com/sudachi808/items/364add4e96a8d6edc82b)
Dagger Hilt: Deep Diveのメモ
(https://qiita.com/takahirom/items/3231edf2a430569b3e9d)
Dependency Injection with Hilt (Kotlin)
(https://blog.devgenius.io/dependency-injection-with-hilt-kotlin-562963c6c779)