Iniezione di dipendenze con Hilt

Hilt è una libreria di dependency injection per Android che riduce il boilerplate dell'esecuzione della dependency injection manuale nel progetto. L'inserimento manuale delle dipendenze richiede la creazione manuale di ogni classe e delle relative dipendenze, nonché l'utilizzo di container per riutilizzare e gestire le dipendenze.

Hilt fornisce un modo standard per utilizzare la DI nella tua applicazione fornendo container per ogni classe Android nel tuo progetto e gestendo automaticamente i relativi cicli di vita. Hilt è basato sulla popolare libreria DI Dagger per sfruttare la correttezza in fase di compilazione, le prestazioni di runtime, la scalabilità e il supporto di Android Studio forniti da Dagger. Per saperne di più, consulta Hilt e Dagger.

Questa guida spiega i concetti di base di Hilt e dei container generati. Include anche una dimostrazione di come eseguire il bootstrap di un'app esistente per utilizzare Hilt.

Aggiunta delle dipendenze

Innanzitutto, aggiungi il plug-in hilt-android-gradle-plugin al file build.gradle nella radice del progetto:

Kotlin

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

Alla moda

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

Poi, applica il plug-in Gradle e aggiungi queste dipendenze nel file 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")
}

Alla moda

...
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"
}

Per assicurarti che il progetto sia configurato per Java 17, richiesto dalle versioni di Jetpack Compose e Hilt, aggiungi quanto segue al file app/build.gradle:

Kotlin

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

Alla moda

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

Classe dell'applicazione Hilt

Tutte le app che utilizzano Hilt devono contenere una classe Application annotata con @HiltAndroidApp.

@HiltAndroidApp attiva la generazione del codice di Hilt, inclusa una classe base per la tua applicazione che funge da container delle dipendenze a livello di applicazione.

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

Questo componente Hilt generato è collegato al ciclo di vita dell'oggetto Application e ne fornisce le dipendenze. Inoltre, è il componente principale dell'app, il che significa che altri componenti possono accedere alle dipendenze che fornisce.

Inserisci le dipendenze nelle classi Android

Una volta configurato Hilt nella classe Application e reso disponibile un componente a livello di applicazione, Hilt può fornire dipendenze ad altre classi Android con l'annotazione @AndroidEntryPoint:

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

Al momento Hilt supporta le seguenti classi Android:

  • Application (utilizzando @HiltAndroidApp)
  • ViewModel (utilizzando @HiltViewModel)
  • Activity
  • Service
  • BroadcastReceiver

In Compose, non è necessario annotare i singoli elementi componibili. Invece, annota il ComponentActivity principale con @AndroidEntryPoint. Questo funge da unico entry point DI per l'intera gerarchia della UI, in modo da poter accedere ai ViewModel inseriti da Hilt direttamente all'interno delle funzioni componibili.

@AndroidEntryPoint genera un singolo componente Hilt per ogni classe Android nel tuo progetto. Questi componenti possono ricevere dipendenze dalle rispettive classi principali, come descritto in Gerarchia dei componenti.

Per ottenere le dipendenze da un componente, utilizza l'annotazione @Inject per eseguire il field injection:

@AndroidEntryPoint
class ExampleActivity : ComponentActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Le classi inserite da Hilt possono avere altre classi base che utilizzano l'inserimento. Queste classi non richiedono l'annotazione @AndroidEntryPoint se sono astratte.

Per scoprire di più sul callback del ciclo di vita in cui viene inserita una classe Android, consulta Durata dei componenti.

Definisci i binding Hilt

Per eseguire il field injection, Hilt deve sapere come fornire istanze delle dipendenze necessarie dal componente corrispondente. Un binding contiene le informazioni necessarie per fornire istanze di un tipo come dipendenza.

Un modo per fornire informazioni di associazione a Hilt è la constructor injection. Utilizza l'annotazione @Inject sul costruttore di una classe per indicare a Hilt come fornire istanze di quella classe:

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

I parametri di un costruttore annotato di una classe sono le dipendenze di quella classe. Nell'esempio, AnalyticsAdapter ha AnalyticsService come dipendenza. Pertanto, Hilt deve anche sapere come fornire istanze di AnalyticsService.

Moduli Hilt

A volte un tipo non può essere inserito tramite constructor injection. Questo può accadere per diversi motivi. Ad esempio, non puoi eseguire il constructor injection di un'interfaccia. Inoltre, non puoi utilizzare il constructor injection per un tipo di cui non sei proprietario, ad esempio una classe di una libreria esterna. In questi casi, puoi fornire a Hilt le informazioni di associazione utilizzando i moduli Hilt.

Un modulo Hilt è una classe annotata con @Module. Fornisce a Hilt istruzioni su come creare istanze di tipi che non possono essere fornite tramite constructor injection, come interfacce o classi di terze parti. Devi anche annotare ogni modulo con @InstallIn per indicare a Hilt in quale classe Android verrà utilizzato o installato ogni modulo.

Le dipendenze che fornisci nei moduli Hilt sono disponibili in tutti i componenti generati associati alla classe Android in cui installi il modulo Hilt.

Inserisci le istanze dell'interfaccia con @Binds

Considera l'esempio di AnalyticsService. Se AnalyticsService è un'interfaccia, non puoi utilizzare la dependency injection del costruttore. Fornisci invece a Hilt le informazioni di associazione creando una funzione astratta annotata con @Binds all'interno di un modulo Hilt.

L'annotazione @Binds indica a Hilt quale implementazione utilizzare quando deve fornire un'istanza di un'interfaccia.

La funzione annotata fornisce a Hilt le seguenti informazioni:

  • Il tipo restituito dalla funzione indica a Hilt di quale interfaccia la funzione fornisce istanze.
  • Il parametro della funzione indica a Hilt quale implementazione fornire.
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
}

Il modulo Hilt AnalyticsModule è annotato con @InstallIn(ActivityComponent.class) perché vuoi che Hilt inserisca questa dipendenza in ExampleActivity. Questa annotazione indica che tutte le dipendenze in AnalyticsModule sono disponibili in tutte le attività dell'app.

Inserisci le istanze con @Provides

Le interfacce non sono l'unico caso in cui non è possibile eseguire l'inserimento del costruttore di un tipo. Inoltre, il constructor injection non è possibile se non sei il proprietario della classe perché proviene da una libreria esterna (classi come Retrofit, OkHttpClient o database Room) o se le istanze devono essere create con il pattern builder.

Considera l'esempio precedente. Se non sei il proprietario diretto della classe AnalyticsService, puoi indicare a Hilt come fornire istanze di questo tipo creando una funzione all'interno di un modulo Hilt e annotandola con @Provides.

La funzione annotata fornisce a Hilt le seguenti informazioni:

  • Il tipo restituito dalla funzione indica a Hilt il tipo di istanze fornite dalla funzione.
  • I parametri della funzione indicano a Hilt le dipendenze del tipo corrispondente.
  • Il corpo della funzione indica a Hilt come fornire un'istanza del tipo corrispondente. Hilt esegue il corpo della funzione ogni volta che deve fornire un'istanza di quel tipo.
@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)
  }
}

Fornisci più associazioni per lo stesso tipo

Nei casi in cui hai bisogno che Hilt fornisca implementazioni diverse dello stesso tipo come dipendenze, devi fornire a Hilt più associazioni. Puoi definire più associazioni per lo stesso tipo con i qualificatori.

Un qualificatore è un'annotazione che utilizzi per identificare un'associazione specifica per un tipo quando per quel tipo sono definite più associazioni.

Considera l'esempio. Se devi intercettare le chiamate a AnalyticsService, puoi utilizzare un oggetto OkHttpClient con un intercettatore. Per altri servizi, potrebbe essere necessario intercettare le chiamate in modo diverso. In questo caso, devi indicare a Hilt come fornire due diverse implementazioni di OkHttpClient.

Innanzitutto, definisci i qualificatori che utilizzerai per annotare i metodi @Binds o @Provides:

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

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

Hilt deve quindi sapere come fornire un'istanza del tipo che corrisponde a ciascun qualificatore. In questo caso, puoi utilizzare un modulo Hilt con @Provides. Entrambi i metodi hanno lo stesso tipo restituito, ma i qualificatori li etichettano come due associazioni diverse:

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

Puoi inserire il tipo specifico di cui hai bisogno annotando il campo o il parametro con il qualificatore corrispondente:

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

Come best practice, se aggiungi un qualificatore a un tipo, aggiungi i qualificatori a tutti i modi possibili per fornire la dipendenza. Lasciare l'implementazione di base o comune senza un qualificatore è un'operazione soggetta a errori e potrebbe comportare l'inserimento della dipendenza errata da parte di Hilt.

Qualificatori predefiniti in Hilt

Hilt fornisce alcuni qualificatori predefiniti. Ad esempio, poiché potresti aver bisogno della classe Context dall'applicazione o dall'attività, Hilt fornisce i qualificatori @ApplicationContext e @ActivityContext.

Supponiamo che la classe AnalyticsAdapter dell'esempio abbia bisogno del contesto dell'attività. Il seguente codice mostra come fornire il contesto dell'attività a AnalyticsAdapter:

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

Per altri binding predefiniti disponibili in Hilt, consulta Binding predefiniti dei componenti.

Componenti generati per le classi Android

Per ogni classe Android in cui puoi eseguire il field injection, esiste un componente Hilt associato a cui puoi fare riferimento nell'annotazione @InstallIn. Ogni componente Hilt è responsabile dell'inserimento dei relativi binding nella classe Android corrispondente.

Gli esempi precedenti hanno mostrato l'utilizzo di ActivityComponent nei moduli Hilt.

Hilt fornisce i seguenti componenti:

Componente Hilt Iniettore per
SingletonComponent Application
ActivityRetainedComponent N/D
ViewModelComponent ViewModel
ActivityComponent Activity
ServiceComponent Service

Durata dei componenti

Hilt crea ed elimina automaticamente le istanze delle classi di componenti generate seguendo il ciclo di vita delle classi Android corrispondenti.

Componente generato Data/ora di creazione Data di distruzione
SingletonComponent Application#onCreate() Application distrutta
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel creato ViewModel distrutta
ActivityComponent Activity#onCreate() Activity#onDestroy()
ServiceComponent Service#onCreate() Service#onDestroy()

Ambiti dei componenti

Per impostazione predefinita, tutte le associazioni in Hilt sono senza ambito. Ciò significa che ogni volta che l'app richiede l'associazione, Hilt crea una nuova istanza del tipo necessario.

Nell'esempio, ogni volta che Hilt fornisce AnalyticsAdapter come dipendenza a un altro tipo o tramite field injection (come in ExampleActivity), Hilt fornisce una nuova istanza di AnalyticsAdapter.

Tuttavia, Hilt consente anche di definire l'ambito di un'associazione a un componente specifico. Hilt crea un'associazione con ambito una sola volta per istanza del componente a cui è associato l'ambito e tutte le richieste per quell'associazione condividono la stessa istanza.

La tabella seguente elenca le annotazioni di ambito per ogni componente generato:

Classe Android Componente generato Ambito
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Service ServiceComponent @ServiceScoped

Nell'esempio, se definisci l'ambito di AnalyticsAdapter per ActivityComponent utilizzando @ActivityScoped, Hilt fornisce la stessa istanza di AnalyticsAdapter per tutta la durata dell'attività corrispondente:

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

Supponiamo che AnalyticsService abbia uno stato interno che richiede l'utilizzo della stessa istanza ogni volta, non solo in ExampleActivity, ma ovunque nell'app. In questo caso, è opportuno limitare l'ambito di AnalyticsService a SingletonComponent. Il risultato è che ogni volta che il componente deve fornire un'istanza di AnalyticsService, fornisce sempre la stessa istanza.

L'esempio seguente mostra come definire l'ambito di un'associazione a un componente in un modulo Hilt. L'ambito di un'associazione deve corrispondere all'ambito del componente in cui è installata, quindi in questo esempio devi installare AnalyticsService in SingletonComponent anziché in 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)
  }
}

Per scoprire di più sugli ambiti dei componenti Hilt, consulta Scoping in Android and Hilt.

Gerarchia dei componenti

L'installazione di un modulo in un componente consente di accedere alle sue associazioni come dipendenza di altre associazioni in quel componente o in qualsiasi componente secondario sottostante nella gerarchia dei componenti.

ActivityComponent è sotto ActivityRetainedComponent. ViewModelComponent si trova sotto ActivityRetainedComponent. ActivityRetainedComponent e ServiceComponent
    sono sotto SingletonComponent.
Figura 1. Gerarchia dei componenti generati da Hilt.

Associazioni predefinite dei componenti

Ogni componente Hilt è dotato di un insieme di associazioni predefinite che Hilt può inserire come dipendenze nelle tue associazioni personalizzate. Tieni presente che queste associazioni corrispondono al tipo di attività generale e non a una sottoclasse specifica. Questo perché Hilt utilizza una singola definizione del componente dell'attività per inserire tutte le attività. Ogni attività ha un'istanza diversa di questo componente.

Componente Android Associazioni predefinite
SingletonComponent Application
ActivityRetainedComponent Application
ViewModelComponent SavedStateHandle
ActivityComponent Application, Activity
ServiceComponent Application, Service

L'associazione del contesto dell'applicazione è disponibile anche tramite @ApplicationContext. Ad esempio:

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

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

L'associazione del contesto dell'attività è disponibile anche utilizzando @ActivityContext. Ad esempio:

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

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

Inserisci le dipendenze nelle classi non supportate da Hilt

In Compose, il pattern standard consiste nell'inserire le dipendenze in un @HiltViewModel utilizzando il constructor injection e quindi nell'utilizzare hiltViewModel() all'interno del composable per accedere al ViewModel. Sebbene Hilt supporti le classi Android più comuni, potresti comunque riscontrare classi non supportate in cui devi eseguire il field injection.

In questi casi, puoi creare un entry point utilizzando l'annotazione @EntryPoint. Un entry point è il confine tra il codice gestito da Hilt e quello che non lo è. È il punto in cui il codice entra per la prima volta nel grafico degli oggetti gestiti da Hilt. I punti di accesso consentono a Hilt di utilizzare codice che non gestisce per fornire dipendenze all'interno del grafico delle dipendenze.

Ad esempio, Hilt non supporta direttamente i provider di contenuti. Se vuoi che un provider di contenuti utilizzi Hilt per ottenere alcune dipendenze, devi definire un'interfaccia annotata con @EntryPoint per ogni tipo di associazione che vuoi e includere i qualificatori. Poi aggiungi @InstallIn per specificare il componente in cui installare l'entry point, come segue:

class ExampleContentProvider : ContentProvider() {

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

  ...
}

Per accedere a un punto di accesso, utilizza il metodo statico appropriato di EntryPointAccessors. Il parametro deve essere l'istanza del componente o l'oggetto @AndroidEntryPoint che funge da contenitore del componente. Assicurati che il componente che passi come parametro e il metodo statico EntryPointAccessors corrispondano entrambi alla classe Android nell'annotazione @InstallIn sull'interfaccia @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()
    ...
  }
}

In questo esempio, devi utilizzare ApplicationContext per recuperare l'entry point perché l'entry point è installato in SingletonComponent. Se l'associazione che volevi recuperare si trovava in ActivityComponent, utilizzeresti invece ActivityContext.

Hilt e Dagger

Hilt è la libreria consigliata ufficialmente per l'inserimento delle dipendenze in Android. Fornisce un modo standardizzato, mirato ed efficiente per implementare la dependency injection nella tua applicazione, ottimizzato in modo specifico per Jetpack Compose e le architetture con una singola attività.

Gli obiettivi di Hilt sono i seguenti:

  • Per creare un set di standard di componenti e ambiti per semplificare la configurazione, la leggibilità e la condivisione del codice tra le app.
  • Per fornire un modo semplice per eseguire il provisioning di associazioni diverse per vari tipi di build, ad esempio test, debug o release.

Poiché il sistema operativo Android crea istanze di molte delle sue classi di framework, l'utilizzo di Dagger in un'app per Android richiede la scrittura di una quantità sostanziale di codice boilerplate. Hilt riduce il codice boilerplate necessario per utilizzare Dagger in un'app per Android. Hilt genera e fornisce automaticamente quanto segue:

  • Componenti per l'integrazione delle classi del framework Android con Dagger che altrimenti dovresti creare manualmente.
  • Annotazioni di ambito da utilizzare con i componenti generati automaticamente da Hilt.
  • Associazioni predefinite per rappresentare classi Android come Application o Activity.
  • Qualificatori predefiniti per rappresentare @ApplicationContext e @ActivityContext.

Il codice Dagger e Hilt può coesistere nello stesso codebase. Tuttavia, nella maggior parte dei casi è meglio utilizzare Hilt per gestire tutto l'utilizzo di Dagger su Android. Per eseguire la migrazione di un progetto che utilizza Dagger a Hilt, consulta la guida alla migrazione.

Risorse aggiuntive

Per scoprire di più su Hilt, consulta le seguenti risorse aggiuntive.

Esempi

Blog

Visualizza contenuti