Nozioni di base sul pugnale

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:

  1. Come creare un'istanza UserRepository con il costruttore con annotazioni @Inject.

  2. Quali sono le sue dipendenze: UserLocalDataSource e UserRemoteDataSource.

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:

  1. 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 stesso LoginUserData.

  2. 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.