Hilt を使用した依存関係挿入

Hilt は Android 用の依存関係インジェクション ライブラリです。これを使うことで、プロジェクトで依存関係の注入(DI)を手動で行うためのボイラープレートが減ります。手動で依存関係の注入を行うには、すべてのクラスとその依存関係を手作業で作成し、コンテナを使用して依存関係の再利用と管理を行う必要があります。

Hilt は、プロジェクト内のすべての Android クラスにコンテナを提供し、そのライフサイクルを自動で管理することで、アプリケーションで DI を行うための標準的な方法を提供します。Hilt は、よく知られた DI ライブラリである Dagger の上に構築されているため、コンパイル時の正確性、実行時のパフォーマンス、スケーラビリティ、Android Studio のサポートといった Dagger の恩恵を受けられます。詳細については、Hilt と Dagger をご覧ください。

このガイドでは、Hilt とそこで生成されるコンテナの基本概念について説明します。また、既存のアプリで Hilt を使用できるようにする方法も紹介します。

依存関係を追加する

まず、hilt-android-gradle-plugin プラグインをプロジェクトのルート build.gradle ファイルに追加します。

Kotlin

plugins {
  ...
  id("com.google.dagger.hilt.android") version "2.57.1" apply false
}

Groovy

plugins {
  ...
  id 'com.google.dagger.hilt.android' version '2.57.1' apply false
}

次に、Gradle プラグインを適用し、app/build.gradle ファイルに次の依存関係を追加します。

Kotlin

plugins {
  id("com.google.devtools.ksp")
  id("com.google.dagger.hilt.android")
}

android {
  ...
}

dependencies {
  implementation("com.google.dagger:hilt-android:2.57.1")
  ksp("com.google.dagger:hilt-android-compiler:2.57.1")
}

Groovy

...
plugins {
  id 'com.google.devtools.ksp'
  id 'com.google.dagger.hilt.android'
}

android {
  ...
}

dependencies {
  implementation "com.google.dagger:hilt-android:2.57.1"
  ksp "com.google.dagger:hilt-compiler:2.57.1"
}

プロジェクトが Jetpack Compose と Hilt のバージョンで必要となる Java 17 用に構成されていることを確認するには、app/build.gradle ファイルに次の内容を追加します。

Kotlin

android {
  ...
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
  }
}

Groovy

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
  }
}

Hilt アプリケーション クラス

Hilt を使用するアプリには、@HiltAndroidApp アノテーションが付けられた Application クラスが含まれている必要があります。

@HiltAndroidApp は、Hilt のコード生成をトリガーします。これには、アプリケーション レベルの依存関係コンテナとして機能するアプリケーションの基本クラスも含まれます。

@HiltAndroidApp
class ExampleApplication : Application() { ... }

ここで生成された Hilt コンポーネントは、Application オブジェクトのライフサイクルにアタッチされ、依存関係を提供します。また、アプリの親コンポーネントであることから、他のコンポーネントがこのコンポーネントの提供する依存関係にアクセスできます。

Android クラスに依存関係を注入する

Application クラスで Hilt がセットアップされ、アプリケーション レベルのコンポーネントが利用可能になると、@AndroidEntryPoint アノテーションが付けられた他の Android クラスに依存関係を提供できるようになります。

@AndroidEntryPoint
class ExampleActivity : ComponentActivity() { ... }

現在、Hilt は以下の Android クラスをサポートしています。

  • Application@HiltAndroidApp を使用)
  • ViewModel@HiltViewModel を使用)
  • Activity
  • Service
  • BroadcastReceiver

Compose では、個々のコンポーザブルにアノテーションを付ける必要はありません。代わりに、ルート ComponentActivity@AndroidEntryPoint アノテーションを付けます。これは UI 階層全体の単一の DI エントリ ポイントとして機能するため、コンポーズ可能な関数内で Hilt によって注入された ViewModel に直接アクセスできます。

@AndroidEntryPoint は、プロジェクト内の Android クラスごとに個別の Hilt コンポーネントを生成します。これらのコンポーネントは、コンポーネント階層で説明されているように、それぞれの親クラスから依存関係を受け取ることができます。

コンポーネントから依存関係を取得するには、@Inject アノテーションを使用してフィールド インジェクションを行います。

@AndroidEntryPoint
class ExampleActivity : ComponentActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Hilt が注入するクラスには、インジェクションを使用する他の基本クラスを含めることができます。抽象クラスの場合、@AndroidEntryPoint アノテーションは不要です。

Android クラスが注入されるライフサイクル コールバックの詳細については、コンポーネントのライフタイムをご覧ください。

Hilt バインディングを定義する

フィールド インジェクションを実行するには、対応するコンポーネントからの必要な依存関係のインスタンス提供方法を Hilt で把握している必要があります。バインディングには、型のインスタンスを依存関係として提供するために必要な情報が含まれています。

Hilt にバインディング情報を提供する方法の 1 つとして、コンストラクタ インジェクションがあります。クラスのコンストラクタで @Inject アノテーションを使用して、そのクラスのインスタンス提供方法を Hilt に知らせます。

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

クラスのアノテーションが付けられたコンストラクタのパラメータは、そのクラスの依存関係です。この例では、AnalyticsAdapter に依存関係として AnalyticsService が指定されています。したがって、Hilt は AnalyticsService インスタンスの提供方法も把握している必要があります。

Hilt モジュール

場合によっては、型へのコンストラクタ インジェクションができないことがあります。これにはいくつかの理由が考えられます。たとえば、インターフェースの場合は、コンストラクタ インジェクションができません。また、外部ライブラリのクラスなど、自分が所有していない型の場合も、コンストラクタ インジェクションができません。このような場合は、Hilt モジュールを使用して、Hilt にバインディング情報を提供します。

Hilt モジュールは、@Module アノテーションが付けられたクラスです。インターフェースやサードパーティ クラスなど、コンストラクタ インジェクションでは提供できない型のインスタンスを作成する方法を Hilt に指示します。また、各モジュールが使用またはインストールされる Android クラスを Hilt に知らせるために、すべてのモジュールに @InstallIn アノテーションを付ける必要があります。

Hilt モジュールで提供する依存関係は、その Hilt モジュールをインストールした Android クラスに関連付けられている、すべての生成されたコンポーネントで使用できます。

@Binds を使用してインターフェース インスタンスを注入する

AnalyticsService の例を考えてみましょう。AnalyticsService がインターフェースの場合は、コンストラクタ インジェクションができません。代わりに、Hilt モジュール内に @Binds アノテーションを付けた抽象関数を作成して、バインディング情報を提供します。

@Binds アノテーションは、インターフェースのインスタンスを提供する必要がある場合に、どの実装を使用するかを知らせるものです。

アノテーションが付いた関数は、次の情報を Hilt に提供します。

  • 関数の戻り値の型で、その関数が提供するインターフェースのインスタンスを知らせます。
  • 関数のパラメータで、提供する実装を知らせます。
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
}

Hilt モジュール AnalyticsModule@InstallIn(ActivityComponent.class) というアノテーションが付いています。これは、依存関係を ExampleActivity に注入させるためです。このアノテーションは、AnalyticsModule 内のすべての依存関係が、アプリのすべてのアクティビティ内で使用できることを意味します。

@Provides を使用してインスタンスを注入する

型のコンストラクタ インジェクションができないのはインターフェースだけではありません。自分で所有していないクラスも、外部ライブラリに含まれているため、コンストラクタ インジェクションができません(RetrofitOkHttpClientRoom データベースなど)。Builder パターンでインスタンスを作成する必要がある場合も同様です。

前の例で考えてみましょう。AnalyticsService クラスを直接所有しない場合は、Hilt モジュール内に関数を作成し、その関数に @Provides アノテーションを付けることにより、この型のインスタンスを提供できます。

アノテーション付きの関数は、次の情報を Hilt に提供します。

  • 関数の戻り値の型で、その関数が提供する型のインスタンスを知らせます。
  • 関数のパラメータは、対応する型の依存関係を知らせます。
  • 関数の本体は、対応する型のインスタンスの提供方法を知らせます。Hilt は、その型のインスタンスを提供する必要があるたびに関数の本体を実行します。
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

同じ型に複数のバインディングを提供する

依存関係として複数の実装を同じ型で提供する必要がある場合は、Hilt で複数のバインディングを用意する必要があります。修飾子を使用することで、同じ型に対して複数のバインディングを定義できます。

修飾子は、1 つの型に複数のバインディングが定義されている場合に、そのタイプの特定のバインディングを識別するために使用するアノテーションです。

具体的な例で考えてみましょう。AnalyticsService の呼び出しをインターセプトする場合は、インターセプタが付いた OkHttpClient オブジェクトを使用できます。他のサービスには、別の方法で呼び出しをインターセプトする必要があります。この場合、OkHttpClient の異なる 2 つの実装の提供方法を Hilt に知らせる必要があります。

まず、@Binds メソッドまたは @Provides メソッドにアノテーションを付けるために使用する修飾子を定義します。

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

次に、Hilt は各修飾子に対応する型のインスタンスの提供方法を把握する必要があります。この場合、@Provides が付いた Hilt モジュールを使用できます。2 つのメソッドでは戻り値の型が同じですが、修飾子によって異なるバインディングとしてラベル付けされます。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

対応する修飾子でフィールドまたはパラメータをアノテーションすることにより、必要とする特定の型を注入できます。

// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: ComponentActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

効果的な手法として、型に修飾子を追加する場合は、その依存関係を提供するすべての方法に対して修飾子を追加することをおすすめします。基本実装または共通実装に修飾子を付けないと、エラーが発生しやすくなり、Hilt が誤った依存関係を注入するかもしれません。

Hilt で事前定義されている修飾子

Hilt では、いくつかの修飾子が事前定義されています。たとえば、アプリケーションまたはアクティビティから Context クラスが必要な場合のために、@ApplicationContext 修飾子と @ActivityContext 修飾子が用意されています。

例にある AnalyticsAdapter クラスでアクティビティのコンテキストが必要であるとします。次のコードは、AnalyticsAdapter にアクティビティのコンテキストの提供方法を示しています。

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

Hilt で利用できる他の事前定義済みバインディングについては、コンポーネントのデフォルトの バインディングをご覧ください。

Android クラスに対して生成されたコンポーネント

フィールド インジェクションができる Android クラスでは、それぞれのクラスごとに、@InstallIn アノテーションで参照できる関連付けされた Hilt コンポーネントがあります。各 Hilt コンポーネントでは、対応する Android クラスにバインディングを注入する必要があります。

これまでの例では、Hilt モジュールでの ActivityComponent の使用方法を示しました。

Hilt には次のコンポーネントが用意されています。

Hilt コンポーネント インジェクションの対象
SingletonComponent Application
ActivityRetainedComponent なし
ViewModelComponent ViewModel
ActivityComponent Activity
ServiceComponent Service

コンポーネントのライフタイム

Hilt は、対応する Android クラスのライフサイクルに従って、生成されたコンポーネント クラスのインスタンスを自動的に作成、破棄します。

生成されたコンポーネント 作成のタイミング 破棄のタイミング
SingletonComponent Application#onCreate() Application を破棄しました
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel を作成しました ViewModel を破棄しました
ActivityComponent Activity#onCreate() Activity#onDestroy()
ServiceComponent Service#onCreate() Service#onDestroy()

コンポーネントのスコープ

デフォルトでは、Hilt のすべてのバインディングはスコープ設定されていません。つまり、アプリがバインディングをリクエストするたびに、必要な型の新しいインスタンスが作成されます。

例では、Hilt が AnalyticsAdapter を別の型の依存関係として提供するたびに、またはフィールド インジェクションを通じて(ExampleActivity として)提供するたびに、AnalyticsAdapter の新しいインスタンスが提供されます。

ただし、Hilt では、バインディングを特定のコンポーネントにスコープ設定することもできます。Hilt は、バインディングの対象となるコンポーネントのインスタンスごとに 1 回だけスコープ設定されたバインディングを作成します。そして、そのバインディングに対するすべてのリクエストで同じインスタンスを共有します。

次の表に、生成された各コンポーネントに対するスコープ アノテーションを示します。

Android クラス 生成されたコンポーネント 範囲
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Service ServiceComponent @ServiceScoped

例では、@ActivityScoped を使用して AnalyticsAdapterActivityComponent にスコープ設定している場合、対応するアクティビティが存続する間、常に AnalyticsAdapter の同じインスタンスが提供されます。

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

AnalyticsService の内部状態が、ExampleActivity の中だけでなくアプリ内のどの場所でも同じインスタンスを使用する必要があるとします。この場合、AnalyticsServiceSingletonComponent にスコープ設定するのが適切です。こうすると、コンポーネントは、AnalyticsService のインスタンスの提供が必要となるたびに、毎回同じインスタンスを提供するようになります。

次の例は、Hilt モジュール内のコンポーネントにバインディングをスコープ設定する方法を示しています。バインディングのスコープは、インストール先のコンポーネントのスコープと一致する必要があるため、この例では ActivityComponent ではなく SingletonComponentAnalyticsService をインストールする必要があります。

// If AnalyticsService is an interface.
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

// If you don't own AnalyticsService.
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {

  @Singleton
  @Provides
  fun provideAnalyticsService(): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

Hilt コンポーネントのスコープについて詳しくは、Android と Hiltでのスコープ設定をご覧ください。

コンポーネント階層

モジュールをコンポーネントにインストールすると、コンポーネント内の他のバインディングの依存関係として、そのバインディングにアクセスできます。また、コンポーネント階層内で下位にある子コンポーネント内の他のバインディングの依存関係としてもアクセスできます。

ActivityComponent は ActivityRetainedComponent の下にある。ViewModelComponent は ActivityRetainedComponent の下にある。ActivityRetainedComponent と ServiceComponent は SingletonComponent の下にある。
図 1. Hilt が生成するコンポーネント階層

コンポーネントのデフォルト バインディング

各 Hilt コンポーネントには、Hilt が独自のカスタム バインディングに依存関係として注入できるデフォルト バインディングのセットが用意されています。このバインディングは、特定のサブクラスではなく、一般のアクティビティ型に対応しています。これは、Hilt では 1 つのアクティビティ コンポーネント定義を使用して、すべてのアクティビティを注入するためです。アクティビティごとに、このコンポーネントの異なるインスタンスがあります。

Android コンポーネント デフォルト バインディング
SingletonComponent Application
ActivityRetainedComponent Application
ViewModelComponent SavedStateHandle
ActivityComponent ApplicationActivity
ServiceComponent ApplicationService

アプリケーション コンテキスト バインディングは @ApplicationContext でも利用できます。次に例を示します。

class AnalyticsServiceImpl @Inject constructor(
  @ApplicationContext context: Context
) : AnalyticsService { ... }

// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
  application: Application
) : AnalyticsService { ... }

アクティビティ コンテキスト バインディングは @ActivityContext でも利用できます。次に例を示します。

class AnalyticsAdapter @Inject constructor(
  @ActivityContext context: Context
) { ... }

// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
  activity: ComponentActivity
) { ... }

Hilt でサポート対象外のクラスに依存関係を注入する

Compose では、標準パターンとして、コンストラクタ インジェクションを使用して @HiltViewModel に依存関係を注入し、コンポーズ可能な関数内で hiltViewModel() を使用して ViewModel にアクセスします。Hilt は一般的な Android クラスのほとんどをサポートしていますが、フィールド インジェクションを行う必要があるサポートされていないクラスに遭遇する可能性もあります。

そのような場合、@EntryPoint アノテーションを使用してエントリ ポイントを作成します。エントリ ポイントは、Hilt が管理するコードとそうでないコードの境界であり、コード内で Hilt で管理するオブジェクトのグラフが開始される最初のポイントです。エントリ ポイントを使用すると、Hilt が管理しないコードを使用して、依存関係グラフ内の依存関係を提供することができます。

たとえば、コンテンツ プロバイダは Hilt で直接サポートされません。コンテンツ プロバイダが Hilt を使用して依存関係を取得するようにしたい場合は、必要なバインディング タイプごとに @EntryPoint アノテーションが付いたインターフェースを定義し、修飾子を含める必要があります。さらに、次のように @InstallIn を追加して、エントリ ポイントをインストールするコンポーネントを指定します。

class ExampleContentProvider : ContentProvider() {

  @EntryPoint
  @InstallIn(SingletonComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }

  ...
}

エントリ ポイントにアクセスするには、EntryPointAccessors の適切な静的メソッドを使用します。パラメータは、コンポーネント インスタンスか、コンポーネント ホルダーとして機能する @AndroidEntryPoint オブジェクトのいずれかです。パラメータとして渡すコンポーネントと EntryPointAccessors 静的メソッドの両方を、@EntryPoint インターフェースの @InstallIn アノテーションで指定した Android クラスと一致させてください。

class ExampleContentProvider: ContentProvider() {
    ...

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)

    val analyticsService = hiltEntryPoint.analyticsService()
    ...
  }
}

この例では、エントリ ポイントが SingletonComponent にインストールされているため、ApplicationContext を使用してエントリ ポイントを取得する必要があります。取得するバインディングが ActivityComponent にある場合は、代わりに ActivityContext を使用します。

Hilt と Dagger

Hilt は、Android で依存関係インジェクションを行うための公式に推奨されているライブラリです。 Jetpack Compose とシングル アクティビティ アーキテクチャ向けに最適化された、標準化された効率的な方法で、アプリケーションに依存関係インジェクションを実装できます。

Hilt の目標は次のとおりです。

  • コンポーネントとスコープの標準セットを作成して、設定とアプリ間でのコード共有を容易し、リーダビリティを向上させる。
  • テスト、デバッグ、リリースなど、さまざまなビルドタイプに対して異なるバインディングをプロビジョニングする簡単な方法を提供する。

Android オペレーティング システムは独自のフレームワーク クラスを数多くインスタンス化するため、Android アプリで Dagger を使用するには大量のボイラープレートを記述する必要があります。Hilt を使用すると、Android アプリで Dagger を使用するときに生じるボイラープレート コードを減らすことができます。Hilt では、次のものが自動的に生成され、提供されます。

  • Dagger を使用して Android フレームワークのクラスを統合するためのコンポーネント(手動で作成する必要がなくなります)。
  • Hilt が自動的に生成するコンポーネントで使用するスコープ アノテーション
  • ApplicationActivity などの Android クラスを表す事前定義されたバインディング
  • @ApplicationContext@ActivityContext を表す事前定義された修飾子

Dagger コードと Hilt コードは、同じコードベース内で共存できます。ただし、ほとんどの場合、Android 上での Dagger の使用は、すべて Hilt で管理することをおすすめします。Dagger を使用するプロジェクトを Hilt に移行するには、移行 ガイドをご覧ください。

参考情報

Hilt の詳細については、次の参考リンクをご覧ください。

サンプル

ブログ

Views のコンテンツ