Blog Tech di Neosperience: l’Innovazione ai Raggi X

Dagger Hilt per la Dependency Injection in Android

Written by Vittorio Riva | Jan 12, 2024 3:49:17 PM

La dependency injection è un principio fondamentale di progettazione del software che mira a semplificare la gestione delle relazioni tra componenti, migliorando la modularità, la manutenibilità e la testabilità del codice.

In Android, dove la complessità delle applicazioni è in costante crescita, la dependency injection si rivela particolarmente vantaggiosa. Questo approccio consente agli sviluppatori di ridurre le dipendenze dirette tra le classi, rendendo il codice più flessibile e suscettibile a modifiche.

Esistono diverse librerie per la gestione della dependency injection in Android:

  • Dagger2: Dagger è una libreria di dependency injection sviluppata da Square e fa parte della famiglia di librerie Dagger. Dagger 2 è la versione successiva di Dagger ed è stata progettata per sfruttare le annotazioni di Java per generare codice di iniezione di dipendenze a tempo di compilazione. Anche se richiede un po' di curva di apprendimento, offre prestazioni ottimali e supporta la generazione di codice efficiente.
  • Guice: Google Guice è un framework di dependency injection che ha guadagnato popolarità prima dell'avvento di Dagger. Guice semplifica la configurazione e l'iniezione di dipendenze attraverso l'uso di annotazioni. Tuttavia, Dagger ha gradualmente superato Guice in termini di adozione, specialmente nel contesto di Android.
  • Dagger Hilt: Dagger Hilt è una libreria di dependency injection specificamente progettata per semplificare l'implementazione della dependency injection in progetti Android. Basata su Dagger, Dagger Hilt introduce concetti come i "Gradle Feature Plugins", semplificando l'organizzazione del codice in moduli e riducendo il boilerplate code. La sua adozione è cresciuta rapidamente nella community Android grazie alla sua facilità d'uso e alle prestazioni.
  • Koin: Koin è un framework di dependency injection scritto in Kotlin per applicazioni Android. Una delle caratteristiche principali di Koin è la sua sintassi dichiarativa e la facilità d'uso. È progettato per essere leggero e offre un'alternativa più semplice per chi preferisce una configurazione meno verbosa rispetto ad altre librerie.

Dagger Hilt

Dagger Hilt, generalmente chiamata Hilt, è l’evolutiva della libreria Dagger, usata in java per la dependency injection. Rispetto al padre, Hilt è stato semplificato per permettere un’integrazione più facile all’interno dei progetti.

Hilt gestisce le dipendenze generando file di codice, con al loro interno i componenti che iniettano le dipendenze dove necessario.

Uno dei vantaggi della generazione del codice durante la fase di build dell’app è che la build viene sospesa se Hilt rileva dei problemi nell’implementazione della dependency injection, permettendo allo sviluppatore di notare e sistemare subito eventuali errori.

Vediamo ora come si implementa Hilt in un progetto Android.

Per prima cosa bisogna aggiungere il plugin Hilt dentro il file build.gradle situato nella cartella root del progetto:


/build.gradle

plugins {
  ...
  id 'com.google.dagger.hilt.android' version '2.44' apply false //plugin di hilt
}

Successivamente bisogna aprire il file build.gradle situato all’interno della cartella app. All’interno di questo file dovremo aggiungere il plugin hilt e aggiungere le dipendenze.


/app/build.gradle

...
plugins {
  id 'kotlin-kapt'
  id 'com.google.dagger.hilt.android' //plugin di hilt
}

android {
  …
  compileOptions { //hilt utilizza java 8, l’inserimento delle compile options ne permette l’utilizzo
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

}

dependencies {
  implementation "com.google.dagger:hilt-android:2.44"
  kapt "com.google.dagger:hilt-compiler:2.44"
}

// Consente i riferimenti al codice generato
kapt {
  correctErrorTypes true
}

}

Arrivati a questo punto possiamo utilizzare Hilt all’interno del progetto.

Tutte le applicazioni che usano Hilt devono avere una classe che estende Application con l’annotazione @HiltAndroidApp.


import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App: Application() {
}

Le annotazioni (annotations in inglese) sono un meccanismo che consente di aggiungere informazioni aggiuntive a vari elementi del codice sorgente. Le annotazioni sono identificate dal simbolo "@", seguito dal nome dell'annotazione.

Le annotazioni svolgono un ruolo cruciale nella semplificazione del processo di sviluppo, facilitando la comunicazione tra lo sviluppatore e il compilatore, il sistema di build o altri strumenti. Esse forniscono istruzioni aggiuntive sul comportamento del codice o su come determinati elementi dovrebbero essere trattati durante la compilazione o l'esecuzione dell'applicazione.

Come effettuare l’injection

Hilt può fornire le dipendenze solo a certi tipi di classi e solo se viene aggiunta l’annotazione @AndroidEntryPoint sopra la definizione della classe.

La lista di classi che supportano @AndroidEntryPoint sono:

  • Application (tramite l’utilizzo di @HiltAndroidApp)
  • ViewModel (tramite l’utilizzo di @HiltViewModel)
  • Activity (solo se estendono ComponentActivity, come ad esempio AppCompatActivity)
  • Fragment (solo se estendono androidx.Fragment)
  • View
  • Service
  • BroadcastReceiver

Una volta che applicheremo l’annotazione @AndroidEntryPoint, potremo fare l’injection delle dependencies tramite l’utilizzo di @Inject, come nell’esempio qui sotto:


@Inject lateinit var userRepository: UserRepository

E’ anche possibile passare una dependency usando l’annotazione @Inject direttamente sul costruttore, come in questo caso:


import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
internal class LogInViewModel @Inject constructor(
    private val navigator: Navigator,
    private val logInUseCase: LogInUseCase,
    private val getUserStateUseCase: GetUserStateUseCase
): BaseViewModelImpl<loginintent, loginaction="">(),
    LifecycleObserver {

I moduli e i componenti

Con @Inject abbiamo visto che possiamo dire a Hilt dove eseguire l’injection, ma in alcuni casi un tipo non può essere iniettato direttamente.

Spesso le classi hanno bisogno di alcuni parametri per essere istanziate, e non possiamo neanche istanziare direttamente un’interfaccia.

Per risolvere questi problemi dobbiamo usare i moduli di Hilt.

I moduli di Hilt sono classi a cui è stata applicata l’annotazione @Module e si usano per informare Hilt su come fornire le istanze di alcuni tipi. Questi moduli vengono visti dai componenti generati da Hilt e ne forniscono le istanze.

Vi sono diversi componenti, ognuno di questi con un lifecycle differente. Conoscere i componenti è molto importante perché ci permette di gestire meglio la memoria eliminando le istanze superflue quando un determinato componente non è più utilizzato.

Ogni component ha uno scope ad esso associato, l’unico scope che un modulo può gestire è quello legato al componente.

I componenti di Hilt e i loro scope sono elencati nella tabella qui sotto, ognuno con il suo lifecycle:

Componenti

Scope

Evento di creazione

Evento di distruzione

SingletonComponent

@Singleton

Application#oncreate()

Quando l’applicazione viene distrutta

ActivityRetainedComponent

@ActivityRetainedScoped

Activity#onCreate()

Activity#onDestroy()

ViewModelComponent

@ViewModelScoped

Quando il ViewModel viene creato

Quando il ViewModel viene distrutto

ActivityComponent

@ActivityScoped

Activity#onCreate()

Activity#onDestroy()

FragmentComponent

@FragmentScoped

Fragment#onAttach()

Fragment#onDestroy()

ViewComponent

@ViewScoped

View#super()

Quando la View viene distrutta

ViewWithFragmentComponent

@ViewScoped

View#super()

Quando la View viene distrutta

ServiceComponent

@ServiceScoped

Service#onCreate()

Service#onDestroy()

Ora che abbiamo visto i moduli e i componenti di Hilt, vediamo come crearne uno:


import javax.inject.Singleton
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

@Module
@InstallIn(SingletonComponent::class)
internal abstract class RepositoriesModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        userRepositoryImpl: UserRepositoryImpl
    ): UserRepository

}

La classe mostrata nell’esempio è astratta e ha un solo metodo definito, utilizzato per fornire l’implementazione della UserRepository.

Analizzando la classe dell’esempio, possiamo notare che ha due annotazioni:

  • @Module, per farla riconoscere da Hilt come modulo
  • @InstallIn(SingletonComponent::class), per legarla al SingletonComponent di Hilt.

Al suo interno definisce un solo metodo, bindUserRepository, con due annotazioni:

  • @Binds, usata per definire i metodi che forniranno l’implementazione di un’interfaccia
  • @Singleton, lo scope del componente in cui sarà visibile questo modulo

Adesso ogni volta che Hilt dovrà fornire il tipo UserRepository, lo inietterà con l’istanza di UserRepositoryImpl. Inoltre, avendo messo l’annotation @Singleton alla funzione, verrà restituita sempre la stessa istanza, facendolo diventare un singleton.

Come detto in precedenza, in alcuni casi dovremo fornire l’istanza di una classe o di un tipo che ha bisogno di essere prima inizializzata in modi specifici.

Per fare questo, possiamo usare l’annotazione @Provides. Questa annotazione può essere usata sui metodi che devono fornire una classe un tipo inizializzato da noi, come nell’esempio qui sotto:


import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent

@Module
@InstallIn(ViewModelComponent::class)
internal class LogInModule {

    @Provides
    fun provideLogIn(
        userNameIsInvalidUseCase: UserNameIsInvalidUseCase,
        passwordIsInvalidUseCase: PasswordIsInvalidUseCase,
        userRepository: UserRepository
    ): LogInUseCase = LogInUseCase {
        username, password ->
        logIn(
            userNameIsInvalidUseCase,
            passwordIsInvalidUseCase,
            userRepository,
            username,
            password,
        )
    **}



}**

private fun logIn(
    userNameIsInvalidUseCase: UserNameIsInvalidUseCase,
    passwordIsInvalidUseCase: PasswordIsInvalidUseCase,
    userRepository: UserRepository,
    username: String,
    password: String
) {

Conclusione

In conclusione, l'implementazione della dependency injection in progetti Android, resa accessibile e efficiente grazie a Dagger Hilt, si rivela una scelta fondamentale per migliorare la modularità, la mantenibilità e la scalabilità del codice.

Affrontare le sfide della dependency injection diventa così un processo più agevole, consentendo agli sviluppatori di concentrarsi maggiormente sulla logica di business e sulla creazione di app robuste e ben strutturate. L'adozione di Dagger Hilt, in aumento da quando è stata aggiunta nella documentazione ufficiale Android per la gestione della dependency injection, rappresenta un passo avanti significativo nella modernizzazione dello sviluppo Android, offrendo una soluzione potente e user-friendly per gestire complessità e dipendenze all'interno delle applicazioni.