Principes de base de Dagger

L'injection de dépendances manuelle ou les localisateurs de services dans une application Android peuvent poser problème en fonction de la taille de votre projet. Vous pouvez limiter la complexité de votre projet à mesure qu'il évolue en utilisant Dagger pour gérer les dépendances.

Dagger génère automatiquement du code qui imite le code que vous auriez écrit manuellement. Comme le code est généré au moment de la compilation, il est vérifiable et plus performant que d'autres solutions basées sur la réflexion telles que Guice.

Avantages de Dagger

Dagger vous dispense d'écrire du code récurrent fastidieux et sujet aux erreurs en vous permettant de procéder comme suit :

  • Générer le code AppContainer (graphique d'application) implémenté manuellement dans la section d'injection de dépendances manuelle.

  • Créer des fabriques pour les classes disponibles dans le graphique d'application. Cela permet de satisfaire les dépendances en interne.

  • Décider s'il faut réutiliser une dépendance ou créer une instance en utilisant des champs d'application.

  • Créer des conteneurs pour des flux spécifiques comme vous l'avez fait avec le flux de connexion dans la section précédente en utilisant les sous-composants Dagger. Cela permet d'améliorer les performances de votre application en libérant la mémoire d'objets lorsqu'ils ne sont plus nécessaires.

Dagger effectue automatiquement toutes ces opérations au moment de la compilation, à condition que vous déclariez les dépendances d'une classe et que vous précisiez comment les satisfaire à l'aide d'annotations. Dagger génère du code semblable à celui que vous auriez écrit manuellement. En interne, Dagger crée un graphique d'objets auquel il peut faire référence pour trouver le moyen de fournir l'instance d'une classe. Pour chaque classe du graphique, Dagger génère une classe de type fabrique qu'il utilise en interne pour obtenir les instances de ce type.

Au moment de la compilation, Dagger vérifie votre code et procède comme suit :

  • Il crée et valide les graphiques de dépendances, ce qui permet de garantir ce qui suit :

    • Les dépendances de chaque objet peuvent être satisfaites. Il n'existe donc aucune exception d'exécution.
    • Comme il n'existe aucun cycle de dépendance, il n'y a aucune boucle infinie.
  • Il génère les classes utilisées lors de l'exécution pour créer les objets réels et leurs dépendances.

Un cas d'utilisation simple dans Dagger : génération d'une fabrique

Pour vous montrer comment utiliser Dagger, nous allons créer une fabrique simple pour la classe UserRepository illustrée dans le diagramme suivant :

Définissez UserRepository comme suit :

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

    ...
}

Ajoutez une annotation @Inject au constructeur UserRepository pour que Dagger sache comment créer 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;
    }
}

Dans l'extrait de code ci-dessus, vous indiquez à Dagger :

  1. Comment créer une instance UserRepository avec le constructeur annoté @Inject.

  2. Ses dépendances : UserLocalDataSource et UserRemoteDataSource.

Dagger sait maintenant comment créer une instance de UserRepository, mais il ne sait pas comment créer ses dépendances. Si vous annotez également les autres classes, Dagger sait comment les créer :

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

Composants Dagger

Dagger peut créer un graphique des dépendances de votre projet pour déterminer où il doit récupérer ces dépendances en cas de besoin. Pour ce faire, vous devez créer une interface et l'annoter avec @Component. Dagger crée un conteneur comme vous l'auriez fait avec l'injection de dépendances manuelle.

Dans l'interface @Component, vous pouvez définir des fonctions qui renvoient des instances des classes dont vous avez besoin (par exemple, UserRepository). @Component indique à Dagger de générer un conteneur avec toutes les dépendances requises pour satisfaire les types qu'il expose. Cela s'appelle un composant Dagger. Il contient un graphique qui se compose des objets que Dagger sait fournir et de leurs dépendances respectives.

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

Lorsque vous créez le projet, Dagger génère une implémentation de l'interface ApplicationGraph à votre place : DaggerApplicationGraph. Grâce à son processeur d'annotations, Dagger crée un graphique de dépendances constitué des relations entre les trois classes (UserRepository, UserLocalDatasource et UserRemoteDataSource) avec un seul point d'entrée : obtenir une instance UserRepository. Vous pouvez l'utiliser comme suit :

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 crée une instance de UserRepository chaque fois que cela est demandé.

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)

Vous avez parfois besoin d'une instance unique d'une dépendance dans un conteneur. Cela peut s'avérer utile pour plusieurs raisons :

  1. Vous souhaitez que d'autres types ayant ce type comme dépendance partagent la même instance, comme plusieurs objets ViewModel dans le flux de connexion utilisant les mêmes LoginUserData.

  2. La création d'un objet est coûteuse et vous ne souhaitez pas créer une instance à chaque fois qu'il est déclaré en tant que dépendance (par exemple, un analyseur JSON).

Dans cet exemple, vous pourriez souhaiter qu'une instance unique de UserRepository soit disponible dans le graphique afin de toujours obtenir la même instance chaque fois que vous demandez un UserRepository. Ceci utile dans votre exemple, car dans une application réelle avec un graphique d'application plus complexe, vous pouvez avoir plusieurs objets ViewModel qui dépendent de UserRepository et ne pas vouloir créer des instances de UserLocalDataSource et UserRemoteDataSource chaque fois que UserRepository doit être fourni.

Dans le cas d'une injection de dépendances manuelle, vous devez transmettre la même instance de UserRepository aux constructeurs des classes ViewModel. Cependant, dans Dagger, comme vous n'écrivez pas ce code manuellement, vous devez indiquer à Dagger que vous souhaitez utiliser la même instance. Pour ce faire, utilisez des annotations de champ d'application.

Définir le champ d'application avec Dagger

Vous pouvez utiliser des annotations de champ d'application pour limiter la durée de vie d'un objet à celle de son composant. Cela signifie que la même instance d'une dépendance est utilisée chaque fois que ce type doit être fourni.

Pour obtenir une instance unique de UserRepository lorsque vous demandez le dépôt dans ApplicationGraph, utilisez la même annotation de champ d'application pour l'interface @Component et UserRepository. Vous pouvez utiliser l'annotation @Singleton déjà fournie avec le package javax.inject que Dagger utilise :

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

Vous pouvez également créer et utiliser une annotation de champ d'application personnalisée. Vous pouvez créer une annotation de champ d'application comme suit :

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

Ensuite, vous pouvez l'utiliser comme avant :

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

Dans les deux cas, le champ d'application de l'objet est identique à celui utilisé pour annoter l'interface @Component. Ainsi, chaque fois que vous appelez applicationGraph.repository(), vous obtenez la même instance de 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)

Conclusion

Il est important de connaître les avantages de Dagger et ses principes de base avant de pouvoir l'utiliser dans des scénarios plus complexes.

Sur la page suivante, vous apprendrez à ajouter Dagger à une application Android.