Injection de dépendances avec Hilt

Hilt est une bibliothèque d'injection de dépendances pour Android qui réduit le code récurrent nécessaire pour injecter manuellement des dépendances dans votre projet. L'injection manuelle de dépendances nécessite de créer chaque classe et ses dépendances manuellement, ainsi que d'utiliser des conteneurs pour réutiliser et gérer les dépendances.

Hilt offre une méthode standard pour utiliser l'injection de dépendances dans votre application en fournissant des conteneurs pour chaque classe Android de votre projet et en gérant automatiquement leur cycle de vie. Hilt repose sur Dagger, une bibliothèque d'injection de dépendances courante, et bénéficie ainsi de l'exactitude du temps de compilation, des performances d'exécution, de l'évolutivité et de la compatibilité avec Android Studio qu'offre Dagger. Pour en savoir plus, consultez la section Hilt et Dagger.

Ce guide décrit les concepts de base de Hilt et de ses conteneurs générés. Il comprend également une démonstration de l'amorçage d'une application existante pour utiliser Hilt.

Ajouter des dépendances

Tout d'abord, ajoutez le plug-in hilt-android-gradle-plugin au fichier build.gradle racine de votre projet :

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
}

Ensuite, appliquez le plug-in Gradle et ajoutez ces dépendances dans votre fichier 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"
}

Pour vous assurer que votre projet est configuré pour Java 17, qui est requis par les versions de Jetpack Compose et Hilt, ajoutez les éléments suivants au fichier 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
  }
}

Classe d'application Hilt

Toutes les applications qui utilisent Hilt doivent contenir une classe Application annotée avec @HiltAndroidApp.

@HiltAndroidApp déclenche la génération du code de Hilt, y compris une classe de base de votre application qui sert de conteneur de dépendances au niveau de l'application.

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

Ce composant Hilt généré est associé au cycle de vie de l'objet Application et lui fournit des dépendances. En outre, il s'agit du composant parent de l'application, ce qui signifie que d'autres composants peuvent accéder aux dépendances qu'il fournit.

Injecter des dépendances dans des classes Android

Une fois que Hilt est configuré dans votre classe Application et qu'un composant au niveau de l'application est disponible, Hilt peut fournir des dépendances à d'autres classes Android comportant l'annotation @AndroidEntryPoint :

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

Hilt est actuellement compatible avec les classes Android suivantes :

  • Application (en utilisant @HiltAndroidApp)
  • ViewModel (en utilisant @HiltViewModel)
  • Activity
  • Service
  • BroadcastReceiver

Dans Compose, vous n'avez pas besoin d'annoter les composables individuels. À la place, annotez votre ComponentActivity racine avec @AndroidEntryPoint. Il s'agit du point d'entrée unique de l'injection de dépendances pour l'ensemble de votre hiérarchie d'UI. Vous pouvez ainsi accéder aux ViewModels injectés par Hilt directement dans vos fonctions composables.

@AndroidEntryPoint génère un composant Hilt individuel pour chaque classe Android de votre projet. Ces composants peuvent recevoir des dépendances provenant de leurs classes parentes respectives, comme décrit dans la section Hiérarchie des composants.

Pour obtenir des dépendances à partir d'un composant, utilisez l'annotation @Inject pour effectuer une injection de champ :

@AndroidEntryPoint
class ExampleActivity : ComponentActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Les classes injectées par Hilt peuvent avoir d'autres classes de base qui utilisent également l'injection. Ces classes ne nécessitent pas l'annotation @AndroidEntryPoint si elles sont abstraites.

Pour en savoir plus sur le rappel de cycle de vie auquel une classe Android est injectée, consultez la section Cycles de vie des composants.

Définir les liaisons de Hilt

Pour effectuer l'injection par champs, Hilt doit savoir comment fournir les instances des dépendances nécessaires à partir du composant correspondant. Une liaison contient les informations nécessaires pour fournir des instances d'un type en tant que dépendance.

L'une des méthodes permettant de fournir des informations de liaison à Hilt est l'injection par constructeur. Utilisez l'annotation @Inject sur le constructeur d'une classe pour indiquer à Hilt comment fournir des instances de cette classe :

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

Les paramètres d'un constructeur annoté d'une classe sont les dépendances de cette classe. Dans l'exemple, AnalyticsAdapter utilise AnalyticsService comme dépendance. Par conséquent, Hilt doit également savoir comment fournir des instances de AnalyticsService.

Modules Hilt

Parfois, un constructeur ne peut pas être injecté dans un type. Les raisons peuvent être multiples. Par exemple, vous ne pouvez pas injecter un constructeur dans une interface. Vous ne pouvez pas non plus injecter un constructeur dans un type qui ne vous appartient pas, comme une classe provenant d'une bibliothèque externe. Dans ces cas, vous pouvez fournir des informations de liaison à Hilt à l'aide de modules Hilt.

Un module Hilt est une classe annotée avec @Module. Il fournit à Hilt des instructions sur la façon de créer des instances de types qui ne peuvent pas être fournies par injection de constructeur, comme les interfaces ou les classes tierces. Vous devez également annoter chaque module avec @InstallIn pour indiquer à Hilt la classe Android dans laquelle chaque module sera utilisé ou installé.

Les dépendances que vous fournissez dans les modules Hilt sont disponibles dans tous les composants générés associés à la classe Android où vous installez le module Hilt.

Injecter des instances d'interface avec "@Binds"

Prenons l'exemple d'AnalyticsService. Si AnalyticsService est une interface, vous ne pouvez pas y injecter un constructeur. Fournissez plutôt les informations de liaison à Hilt en créant une fonction abstraite annotée avec @Binds dans un module Hilt.

L'annotation @Binds indique à Hilt l'implémentation à utiliser lorsqu'il doit fournir une instance d'une interface.

La fonction annotée fournit les informations suivantes à Hilt :

  • Le type renvoyé de la fonction indique à Hilt l'interface dont la fonction fournit des instances.
  • Le paramètre de fonction indique à Hilt l'implémentation à fournir.
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
}

Le module Hilt AnalyticsModule est annoté avec @InstallIn(ActivityComponent.class), car vous souhaitez que Hilt injecte cette dépendance dans ExampleActivity. Cette annotation signifie que toutes les dépendances de AnalyticsModule sont disponibles dans toutes les activités de l'application.

Injecter des instances avec "@Provides"

Les interfaces ne sont pas le seul cas dans lequel vous ne pouvez pas injecter un constructeur dans un type. Il est également impossible d'injecter un constructeur si vous ne possédez pas la classe, car elle provient d'une bibliothèque externe (classes comme Retrofit, OkHttpClient ou les bases de données Room), ou si des instances doivent être créées avec le modèle de compilateur.

Prenons l'exemple précédent. Si vous ne possédez pas directement la classe AnalyticsService, vous pouvez indiquer à Hilt comment fournir des instances de ce type en créant une fonction dans un module Hilt et en l'annotant avec @Provides.

La fonction annotée fournit les informations suivantes à Hilt :

  • Le type renvoyé de la fonction indique à Hilt le type dont la fonction fournit des instances.
  • Les paramètres de la fonction indiquent à Hilt les dépendances du type correspondant.
  • Le corps de la fonction indique à Hilt comment fournir une instance du type correspondant. Hilt exécute le corps de la fonction chaque fois qu'il doit fournir une instance de ce type.
@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)
  }
}

Fournir plusieurs liaisons pour le même type

Si vous avez besoin que Hilt fournisse différentes implémentations d'un même type en tant que dépendances, vous devez lui fournir plusieurs liaisons. Vous pouvez définir plusieurs liaisons pour le même type à l'aide de qualificatifs.

Un qualificatif est une annotation qui permet d'identifier une liaison spécifique d'un type lorsque plusieurs liaisons sont définies pour ce type.

Prenons cet exemple. Si vous devez intercepter les appels vers AnalyticsService, vous pouvez utiliser un objet OkHttpClient avec un intercepteur. Pour les autres services, vous devrez peut-être intercepter les appels d'une autre manière. Dans ce cas, vous devez indiquer à Hilt comment fournir deux implémentations différentes de OkHttpClient.

Tout d'abord, définissez les qualificatifs que vous utiliserez pour annoter les méthodes @Binds ou @Provides :

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

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

Ensuite, Hilt doit savoir comment fournir une instance du type correspondant à chaque qualificatif. Dans ce cas, vous pouvez utiliser un module Hilt avec @Provides. Le type renvoyé est identique pour les deux méthodes, mais les qualificatifs les associent à deux liaisons différentes :

@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()
  }
}

Vous pouvez injecter le type spécifique dont vous avez besoin en annotant le champ ou le paramètre avec le qualificatif correspondant :

// 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
}

Si vous ajoutez un qualificatif à un type, nous vous recommandons d'ajouter des qualificatifs à toutes les méthodes possibles permettant de fournir cette dépendance. Une implémentation de base ou commune sans qualificatif risque d'entraîner des erreurs et pourrait provoquer l'injection d'une mauvaise dépendance par Hilt.

Qualificatifs prédéfinis dans Hilt

Hilt fournit quelques qualificatifs prédéfinis. Par exemple, comme vous aurez peut-être besoin de la classe Context de l'application ou de l'activité, Hilt fournit les qualificatifs @ApplicationContext et @ActivityContext.

Supposons que la classe AnalyticsAdapter de l'exemple ait besoin du contexte de l'activité. Le code suivant montre comment fournir le contexte de l'activité à AnalyticsAdapter :

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

Pour les autres liaisons prédéfinies disponibles dans Hilt, consultez la section Liaisons par défaut des composants.

Composants générés pour les classes Android

Pour chaque classe Android dans laquelle vous pouvez effectuer une injection de champ, un composant Hilt associé est disponible dans l'annotation @InstallIn. Chaque composant Hilt est chargé d'injecter ses liaisons dans la classe Android correspondante.

Les exemples précédents ont illustré l'utilisation d'ActivityComponent dans les modules Hilt.

Hilt fournit les composants suivants :

Composant Hilt Injecteur pour
SingletonComponent Application
ActivityRetainedComponent N/A
ViewModelComponent ViewModel
ActivityComponent Activity
ServiceComponent Service

Cycles de vie des composants

Hilt crée et détruit automatiquement des instances de classes de composants générés en suivant le cycle de vie des classes Android correspondantes.

Composant généré Créé dans Détruit dans
SingletonComponent Application#onCreate() Application détruite
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel créé ViewModel détruite
ActivityComponent Activity#onCreate() Activity#onDestroy()
ServiceComponent Service#onCreate() Service#onDestroy()

Champs d'application des composants

Par défaut, toutes les liaisons dans Hilt n'ont pas de champ d'application. Cela signifie que, chaque fois que votre application demande la liaison, Hilt crée une instance du type requis.

Dans l'exemple, chaque fois que Hilt fournit AnalyticsAdapter en tant que dépendance d'un autre type ou via l'injection par champs (comme dans ExampleActivity), Hilt fournit une nouvelle instance d'AnalyticsAdapter.

Toutefois, Hilt permet également de définir le champ d'application d'une liaison sur un composant particulier. Hilt crée un champ d'application de liaison une seule fois par instance du composant sur lequel le champ d'application de liaison est défini. Toutes les demandes pour cette liaison partagent la même instance.

Le tableau ci-dessous indique les annotations de champ d'application pour chaque composant généré :

Classe Android Composant généré Champ d'application
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Service ServiceComponent @ServiceScoped

Dans l'exemple, si vous définissez le champ d'application d'AnalyticsAdapter sur l'ActivityComponent à l'aide d'@ActivityScoped, Hilt fournit la même instance d'AnalyticsAdapter tout au long du cycle de vie de l'activité correspondante :

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

Supposons qu'AnalyticsService ait un état interne nécessitant que la même instance soit utilisée à chaque fois, non seulement dans ExampleActivity, mais aussi dans toute l'application. Dans ce cas, il est approprié de définir le champ d'application d'AnalyticsService sur SingletonComponent. Par conséquent, lorsque le composant doit fournir une instance d'AnalyticsService, il fournit la même instance à chaque fois.

L'exemple suivant montre comment définir le champ d'application d'une liaison sur un composant d'un module Hilt. Le champ d'application d'une liaison doit correspondre à celui du composant où elle est installée. Dans cet exemple, vous devez donc installer AnalyticsService dans SingletonComponent au lieu d'ActivityComponent :

// 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)
  }
}

Pour en savoir plus sur les champs d'application des composants Hilt, consultez la page Définition du champ d'application dans Android et Hilt.

Hiérarchie des composants

L'installation d'un module dans un composant permet d'accéder à ses liaisons en tant que dépendance d'autres liaisons dans ce composant ou dans tout composant enfant inférieur dans la hiérarchie des composants.

"ActivityComponent" se trouve sous "ActivityRetainedComponent". "ViewModelComponent" se trouve sous "ActivityRetainedComponent". "ActivityRetainedComponent" et "ServiceComponent" se trouvent sous "SingletonComponent".
Figure 1. Hiérarchie des composants générés par Hilt.

Liaisons par défaut des composants

Chaque composant Hilt est fourni avec un ensemble de liaisons par défaut que Hilt peut injecter en tant que dépendances dans vos propres liaisons personnalisées. Notez que ces liaisons correspondent à un type d'activité général, et non à une sous-classe spécifique. En effet, Hilt utilise une seule définition de composant d'activité pour injecter toutes les activités. Chaque activité est associée à une instance différente de ce composant.

Composant Android Liaisons par défaut
SingletonComponent Application
ActivityRetainedComponent Application
ViewModelComponent SavedStateHandle
ActivityComponent Application, Activity
ServiceComponent Application, Service

La liaison de contexte de l'application est également disponible à l'aide de @ApplicationContext. Exemple :

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

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

La liaison de contexte de l'activité est également disponible à l'aide de @ActivityContext. Exemple :

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

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

Injecter des dépendances dans des classes non compatibles avec Hilt

Dans Compose, le modèle standard consiste à injecter des dépendances dans un @HiltViewModel à l'aide de l'injection par constructeur, puis à utiliser hiltViewModel() dans votre composable pour accéder au ViewModel. Bien que Hilt soit compatible avec les classes Android les plus courantes, vous pouvez toujours rencontrer des classes non compatibles dans lesquelles vous devez effectuer une injection par champs.

Dans ces cas, vous pouvez créer un point d'entrée à l'aide de l'annotation @EntryPoint. Un point d'entrée est la limite entre le code géré par Hilt et le code qui ne l'est pas. Il s'agit du point au niveau duquel le code entre pour la première fois dans le graphique des objets gérés par Hilt. Les points d'entrée permettent à Hilt d'utiliser du code qu'il ne gère pas pour fournir des dépendances dans le graphique de dépendances.

Par exemple, Hilt ne prend pas directement en charge les fournisseurs de contenu. Si vous souhaitez qu'un fournisseur de contenu utilise Hilt pour obtenir des dépendances, vous devez définir une interface annotée avec @EntryPoint pour chaque type de liaison souhaité et inclure des qualificatifs. Ajoutez ensuite @InstallIn pour spécifier le composant dans lequel installer le point d'entrée, comme suit :

class ExampleContentProvider : ContentProvider() {

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

  ...
}

Pour accéder à un point d'entrée, utilisez la méthode statique appropriée depuis EntryPointAccessors. Le paramètre doit correspondre soit à l'instance du composant, soit à l'objet @AndroidEntryPoint qui sert de conteneur de composants. Assurez-vous que le composant transmis en tant que paramètre et la méthode statique EntryPointAccessors correspondent tous deux à la classe Android dans l'annotation @InstallIn sur l'interface @EntryPoint :

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()
    ...
  }
}

Dans cet exemple, vous devez utiliser l'ApplicationContext pour récupérer le point d'entrée, car il est installé dans SingletonComponent. Si la liaison que vous souhaitez récupérer se trouve dans ActivityComponent, utilisez plutôt l'ActivityContext.

Hilt et Dagger

Hilt est la bibliothèque officiellement recommandée pour l'injection de dépendances dans Android. Il fournit un moyen standardisé, orienté et efficace d'implémenter l'injection de dépendances dans votre application, en particulier optimisé pour Jetpack Compose et les architectures à activité unique.

Les objectifs de Hilt sont les suivants :

  • Créer un ensemble standard de composants et de champs d'application pour faciliter la configuration, la lisibilité et le partage de code entre les applications.
  • Offrir un moyen simple de fournir différentes liaisons pour divers types de compilation, tels que des tests, des débogages ou des versions.

Étant donné que le système d'exploitation Android instancie un grand nombre de ses propres classes de framework, l'utilisation de Dagger dans une application Android nécessite une quantité importante de code récurrent. Hilt réduit le code récurrent nécessaire à l'utilisation de Dagger dans une application Android. Hilt génère et fournit automatiquement les éléments suivants :

  • Des composants permettant d'intégrer des classes du framework Android avec Dagger, qu'il faudrait sinon créer manuellement.
  • Des annotations de champ d'application à utiliser avec les composants générés automatiquement par Hilt.
  • Des liaisons prédéfinies pour représenter des classes Android telles que Application ou Activity.
  • Des qualificatifs prédéfinis pour représenter @ApplicationContext et @ActivityContext.

Le code de Dagger et celui de Hilt peuvent coexister dans le même codebase. Toutefois, dans la plupart des cas, il est préférable d'utiliser Hilt pour gérer l'ensemble de votre utilisation de Dagger sur Android. Pour migrer un projet qui utilise Dagger vers Hilt, consultez le guide de migration.

Ressources supplémentaires

Pour en savoir plus sur Hilt, consultez les ressources supplémentaires suivantes.

Exemples

Blogs

Afficher le contenu