Androidで主に使用される依存性注入は、コンストラクタインジェクションとフィールドインジェクション。
用語
- コンストラクタインジェクション
クラスの生成時に依存させるインスタンス(使うインスタンス)を渡す方法 - フィールドインジェクション
クラスの生成後に依存させるインスタンス(使うインスタンス)をsetterメソッドで渡す方法
ActivityやFragmentの引数を変更することはできないのでこの方法を採る。
インジェクションの基本
- Applicationクラスに「@HiltAndroidApp」をつける(Hiltが使用可能になる)
- インジェクションされる側のクラスに「@AndroidEntryPoint」をつけ、
インスタンスを格納するフィールドに「@Inject」をつける(フィールドインジェクション)。lateinit宣言も必要。 - フィールドに注入したい(バインディングしたい)クラスのコンストラクタに「@Inject」をつける
(コンストラクタインジェクションを用いることでHiltにバインディング情報を伝える)
(注意1)
クラスに@AndroidEntryPointを付けると、個別の Hilt コンポーネントが生成される
(注意2)
@AndroidEntryPointを付ける場合は、それに依存するAndroidクラスにもアノテーションを付ける必要がある。たとえば、フラグメントにアノテーションを付ける場合は、そのフラグメントを使用するアクティビティにもアノテーションを付ける必要がある
(注意3)
@AndroidEntryPointをつけれるクラスは限定されている。
Hiltでサポートされていないクラスには、フィールドインジェクションを行う。参考
HiltでサポートされているAndroidクラス
クラス名 | |
---|---|
Application | @HiltAndroidApp |
ViewModel | @HiltViewModel |
Activity | @AndroidEntryPoint |
Fragment | @AndroidEntryPoint |
View | @AndroidEntryPoint |
Service | @AndroidEntryPoint |
BroadcastReceiver | @AndroidEntryPoint |
(注意4)
「@Inject」をつけたクラスのコンストラクタが引数を持つ場合は、引数のクラスにも「@Inject」をつける必要がある?->Yes.
例
//基本の1と2を実施
@AndroidEntryPoint
class LogsFragment : Fragment() {
//lateinitすると初期化後はnon-nullが保証される
@Inject lateinit var dateFormatter: DateFormatter
・・・
}
//基本の3を実施
class DateFormatter @Inject constructor() { ... }
上のような依存関係となり、LogsFragmentとDateFormatterには依存関係がない。これがHiltを使用するメリット
スコープ
フィールドインジェクションができるAndroidクラスには対応するHiltコンポーネントがある。このHiltコンポーネントにバインディング(クラスのインスタンスの情報)を紐つけることでスコープ設定を実現する。
デフォルトでは、バインディングにはスコープ設定されない。アプリがバインディングをリクエストするたびに、必要な型の新しいインスタンスが作成されるが、バインディングしたいクラスの上にスコープアノテーションをつけると、対応するコンポーネントのインスタンスごとに1回だけ作成される。
例、バインディングする(コンストラクタインジェクションしている)Adapterクラスに@ActivityScopedを設定している場合、対応するアクティビティが存続する間、常に同じAdapterインスタンスが提供される。
スコープアノテーションを付けたバインディングは以下の対応関係でコンポーネントに紐付き、ライフタイムはAndroidクラスのライフサイクルと関係する。onCreate()で生成されonDestroy()で破棄等。詳細は公式ドキュメントで。
Androidクラス | @InstallInで参照できるHiltコンポーネント | スコープ |
---|---|---|
Application | SingletonComponent | @Singleton |
Activity | ActivityRetainedComponent | @ActivityRetainedScope |
View Model | ViewModelComponent | @ViewModelScoped |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
View | ViewComponent | @ViewScoped |
@WithFragmentBindings アノテーションが付いたView | ViewWithFragmentComponent | @ViewScoped |
Service | ServiceComponent | @ServiceScoped |
ActivityのスコープをSingletonComponentにスコープ設定したい場合、バインディングのスコープは、インストール先のコンポーネントのスコープと一致する必要があるため、@InstallIn(SingletonComponent::class)とする
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
↓
この例あっている?
@InstallIn(SingletonComponent::class)
@Singleton
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
「コンポーネントのデフォルト バインディング」の項は謎。
参考URL
https://developer.android.com/training/dependency-injection/hilt-android#generated-components
Androidクラス | コンポーネント |
---|---|
@InstallIn | Hiltがコンポーネントを生成する際に、アノテーションされたクラスをどのコンポーネントに含めるかを宣言するアノテーションです。 |
@ |
ここから下は応用。
Hiltモジュール
インターフェース、ソースコード外のクラスは、基本の3であるバインディング情報をHiltに伝えられない。 コンストラクタインジェクションができない。 このような場合は、Hiltモジュールを使う。
(注意)
スコープが異なるコンテナに設定されている場合、同じモジュールを使用することはできない。
自分で所有していないクラスの場合
- @Moduleをつける
- @InstallInで、バインディングを使用できるコンポーネント(ActivityComponent等)を指示
- 自分で所有していないクラスを返却するメソッドに@Providesをつける
@InstallIn(SingletonComponent::class)
@Module
object MyModule {
@Provides
fun f(@ApplicationContext appContext: Context): Context {
return appContext
}
}
Providesする関数の命名に法則はない。
返却する型が合致してさえいればよい。
コンストラクタインジェクションなのでバッティングすることはない。
InstallInは、SingletonComponent他、ActivityComponentがある
interfaceを返すクラスの場合
- Hiltモジュールファイルを作る(モジュールファイルには@Bindsアノテーションと@Providesアノテーションは共存できない)
- @Binds アノテーションを付けた抽象関数を作成して、バインディング情報を提供
// この例は謎。
interface AnalyticsService {
fun analyticsMethods()
}
// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
同じ型の異なる実装(複数のバインディング)を提供する方法
@Qualifierで識別させる
@Qualifier
annotation class InMemoryLogger
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger//独自アノテーション
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
ContentProviderにインジェクションする場合
ContentProviderはHiltでサポートされないため、エントリーポイントを@AndroidEntryPointで指定できない。
- ContentProvider内にをバインディングしたいコンポーネントを返すインターフェースを作成する
- 上記インターフェイスに@EntryPointをつける
- エントリポイントにアクセスするには、EntryPointAccessorsを使う
class LogsContentProvider: ContentProvider() {
@InstallIn(SingletonComponent::class)
@EntryPoint
interface LCPEntryPoint {
fun logDao(): LogDao
}
・・・
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LCPEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}