L'inserimento manuale delle dipendenze o i localizzatori di servizi in un'app per Android possono essere problematici a seconda delle dimensioni del progetto. Puoi limitare la complessità del progetto durante lo scale up utilizzando Dagger per gestire le dipendenze.
Dagger genera automaticamente un codice che imita il codice che altrimenti avresti scritto a mano. Poiché il codice viene generato al momento della compilazione, è tracciabile e offre prestazioni più elevate rispetto ad altre soluzioni basate sulla riflessione come Guice.
Vantaggi dell'utilizzo di Dagger
Dagger ti evita di scrivere codice boilerplate noioso e soggetto a errori:
Generazione del codice
AppContainer
(grafico dell'applicazione) che hai implementato manualmente nella sezione DI manuale.Creazione di fabbriche per le classi disponibili nel grafico dell'applicazione. Ecco come vengono soddisfatte internamente le dipendenze.
Decidere se riutilizzare una dipendenza o creare una nuova istanza tramite l'utilizzo degli ambiti.
Creazione di container per flussi specifici come hai fatto con il flusso di accesso nella sezione precedente utilizzando i sottocomponenti Dagger. Questo migliora le prestazioni della tua app rilasciando oggetti in memoria quando non sono più necessari.
Dagger esegue automaticamente tutte queste operazioni al momento della creazione, a condizione che dichiari le dipendenze di una classe e specifichi come soddisfarle utilizzando le annotazioni. Dagger genera un codice simile a quello che avresti scritto manualmente. Internamente, Dagger crea un grafico di oggetti a cui può fare riferimento per trovare il modo di fornire un'istanza di una classe. Per ogni classe nel grafico, Dagger genera una classe [Factory-type] che utilizza internamente per recuperare istanze di quel tipo.
Durante la creazione, Dagger analizza il tuo codice e:
Crea e convalida grafici delle dipendenze, garantendo che:
- Le dipendenze di ogni oggetto possono essere soddisfatte, quindi non ci sono eccezioni di runtime.
- Non esistono cicli di dipendenza, quindi non ci sono loop infiniti.
Genera le classi utilizzate in fase di runtime per creare gli oggetti effettivi e le relative dipendenze.
Un semplice caso d'uso in Dagger: generare una fabbrica
Per dimostrare come puoi lavorare con Dagger, creiamo una semplice fabbrica per la classe UserRepository
mostrata nel diagramma seguente:
Definisci UserRepository
come segue:
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
Aggiungi un'annotazione @Inject
al costruttore UserRepository
in modo che Dagger sappia come creare un UserRepository
:
Kotlin
// @Inject lets Dagger know how to create instances of this object class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; // @Inject lets Dagger know how to create instances of this object @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } }
Nello snippet di codice riportato sopra, stai dicendo a Dagger:
Come creare un'istanza
UserRepository
con il costruttore con annotazioni@Inject
.Quali sono le sue dipendenze:
UserLocalDataSource
eUserRemoteDataSource
.
Ora Dagger sa come creare un'istanza di UserRepository
, ma non sa come creare le sue dipendenze. Se aggiungi annotazioni anche alle altre classi,
Dagger sa come crearle:
Kotlin
// @Inject lets Dagger know how to create instances of these objects class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor() { ... }
Java
public class UserLocalDataSource { @Inject public UserLocalDataSource() { } } public class UserRemoteDataSource { @Inject public UserRemoteDataSource() { } }
Componenti Dagger
Dagger può creare un grafico delle dipendenze nel progetto che può utilizzare per scoprire dove trova queste dipendenze quando sono necessarie.
Per consentire a Dagger di eseguire questa operazione, devi creare un'interfaccia e annotarla con @Component
. Dagger crea un container come faresti con l'inserimento manuale delle dipendenze.
Nell'interfaccia @Component
puoi definire funzioni che restituiscono istanze delle classi necessarie (ad es. UserRepository
). @Component
indica a Dagger di generare un container con tutte le dipendenze necessarie per soddisfare i tipi che espone. Questo componente è chiamato componente Dagger e contiene un grafico costituito dagli oggetti che Dagger è in grado di fornire e dalle rispettive dipendenze.
Kotlin
// @Component makes Dagger create a graph of dependencies @Component interface ApplicationGraph { // The return type of functions inside the component interface is // what can be provided from the container fun repository(): UserRepository }
Java
// @Component makes Dagger create a graph of dependencies @Component public interface ApplicationGraph { // The return type of functions inside the component interface is // what can be consumed from the graph UserRepository userRepository(); }
Quando crei il progetto, Dagger genera automaticamente un'implementazione dell'interfaccia ApplicationGraph
: DaggerApplicationGraph
. Con il suo processore di annotazione, Dagger crea un grafico delle dipendenze composto dalle relazioni tra le tre classi (UserRepository
, UserLocalDatasource
e UserRemoteDataSource
) con un solo punto di ingresso: ottenere un'istanza UserRepository
. Puoi utilizzarlo nel seguente modo:
Kotlin
// Create an instance of the application graph val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create() // Grab an instance of UserRepository from the application graph val userRepository: UserRepository = applicationGraph.repository()
Java
// Create an instance of the application graph ApplicationGraph applicationGraph = DaggerApplicationGraph.create(); // Grab an instance of UserRepository from the application graph UserRepository userRepository = applicationGraph.userRepository();
Dagger crea una nuova istanza di UserRepository
ogni volta che viene richiesta.
Kotlin
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create() val userRepository: UserRepository = applicationGraph.repository() val userRepository2: UserRepository = applicationGraph.repository() assert(userRepository != userRepository2)
Java
ApplicationGraph applicationGraph = DaggerApplicationGraph.create(); UserRepository userRepository = applicationGraph.userRepository(); UserRepository userRepository2 = applicationGraph.userRepository(); assert(userRepository != userRepository2)
A volte, è necessario avere un'istanza univoca di una dipendenza in un container. Questa opzione può essere utile per diversi motivi:
Vuoi che altri tipi che hanno questo tipo come dipendenza condividano la stessa istanza, ad esempio più oggetti
ViewModel
nel flusso di accesso che utilizzano lo stessoLoginUserData
.Un oggetto è costoso da creare e non vuoi creare una nuova istanza ogni volta che viene dichiarato come dipendenza (ad esempio, un parser JSON).
Nell'esempio, potresti voler avere un'istanza univoca di UserRepository
disponibile nel grafico in modo che ogni volta che richiedi un UserRepository
, ricevi sempre la stessa istanza. Questo è utile nell'esempio perché in un'applicazione reale con un grafico dell'applicazione più complesso, potresti avere più oggetti ViewModel
a seconda di UserRepository
e non vuoi creare nuove istanze di UserLocalDataSource
e UserRemoteDataSource
ogni volta che è necessario fornire UserRepository
.
Nell'inserimento manuale delle dipendenze, puoi farlo passando la stessa istanza di UserRepository
ai costruttori delle classi ViewModel. In Dagger, invece, dato che non stai scrivendo quel codice manualmente, devi comunicare a Dagger che vuoi utilizzare la stessa istanza. A questo scopo, utilizza le annotazioni dell'ambito.
Definizione dell'ambito con Pugnale
Puoi utilizzare le annotazioni dell'ambito per limitare la durata di un oggetto alla durata del suo componente. Ciò significa che viene utilizzata la stessa istanza di una dipendenza ogni volta che è necessario fornire quel tipo.
Per avere un'istanza univoca di UserRepository
quando richiedi il repository in ApplicationGraph
, utilizza la stessa annotazione dell'ambito per l'interfaccia @Component
e UserRepository
. Puoi utilizzare l'annotazione @Singleton
che
già viene fornita con il pacchetto javax.inject
utilizzato da Dagger:
Kotlin
// Scope annotations on a @Component interface informs Dagger that classes annotated // with this annotation (i.e. @Singleton) are bound to the life of the graph and so // the same instance of that type is provided every time the type is requested. @Singleton @Component interface ApplicationGraph { fun repository(): UserRepository } // Scope this class to a component using @Singleton scope (i.e. ApplicationGraph) @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... }
Java
// Scope annotations on a @Component interface informs Dagger that classes annotated // with this annotation (i.e. @Singleton) are scoped to the graph and the same // instance of that type is provided every time the type is requested. @Singleton @Component public interface ApplicationGraph { UserRepository userRepository(); } // Scope this class to a component using @Singleton scope (i.e. ApplicationGraph) @Singleton public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } }
In alternativa, puoi creare e utilizzare un'annotazione con ambito personalizzato. Puoi creare un'annotazione di ambito nel seguente modo:
Kotlin
// Creates MyCustomScope @Scope @MustBeDocumented @Retention(value = AnnotationRetention.RUNTIME) annotation class MyCustomScope
Java
// Creates MyCustomScope @Scope @Retention(RetentionPolicy.RUNTIME) public @interface MyCustomScope {}
Dopodiché puoi utilizzarlo come prima:
Kotlin
@MyCustomScope @Component interface ApplicationGraph { fun repository(): UserRepository } @MyCustomScope class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val service: UserService ) { ... }
Java
@MyCustomScope @Component public interface ApplicationGraph { UserRepository userRepository(); } @MyCustomScope public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } }
In entrambi i casi, all'oggetto viene fornito lo stesso ambito utilizzato per annotare l'interfaccia @Component
. In questo modo, ogni volta che chiami applicationGraph.repository()
, ottieni la stessa istanza di UserRepository
.
Kotlin
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create() val userRepository: UserRepository = applicationGraph.repository() val userRepository2: UserRepository = applicationGraph.repository() assert(userRepository == userRepository2)
Java
ApplicationGraph applicationGraph = DaggerApplicationGraph.create(); UserRepository userRepository = applicationGraph.userRepository(); UserRepository userRepository2 = applicationGraph.userRepository(); assert(userRepository == userRepository2)
conclusione
Prima di poterlo utilizzare in scenari più complessi, è importante conoscere i vantaggi di Dagger e le nozioni di base sul suo funzionamento.
Nella pagina successiva scoprirai come aggiungere Dagger a un'app per Android.