Introduction à Dagger2

1 – Introduction

Dagger2 est la deuxième version de l’outil d’injection de dépendances Dagger, initialement porté par Square (aussi connus pour Retrofit et Picasso), puis reprit par Google : http://google.github.io/dagger/

Injection de dépendance ?

L’injection de dépendance est un mécanisme qui consiste à créer dynamiquement (injecter) les dépendances entre différentes classes. Ainsi grâce à cette méthode les dépendances entre les différents composants ne sont plus exprimées de manière statique mais determines dynamiquement.

Importer Dagger2

Dagger fonctionne en utilisant le système d’annotations. Il faudra donc importer le module gradle android-apt, permettant une meilleur gestion des annotations sur Android Studio.

Android-apt est un plugin gradle, il faut donc ajouter com.neenbedankt.gradle.plugins:android-apt aux dépendances du /build.gradle de notre projet (et non celui du module !)

/build.gradle

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'

        //annotation processor tool
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
    }
}

Afin d’appliquer le plugin, il faut ajouter l’instruction apply plugin: ‘com.neenbedankt.android-apt’ en haut du build.gradle du module principal du projet. Cela permet d’utiliser dans le build.gradle l’instruction apt.

app/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
...

dependencies{
    compile 'com.google.dagger:dagger:2.0.1'
    apt 'com.google.dagger:dagger-compiler:2.0.1' //apt utilisera dagger-compiler
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

Il faut ajouter aussi la bibliothèque javax.annotations, contenant les annotations @Inject et @Provides.


2 – Mise en contexte

Nous allons reprendre l’exemple introduit dans le README de Retrofit (communiquer avec le webservice de Github).

http://square.github.io/retrofit/

Capture d’écran 2015-06-04 à 15.57.42

Réaliser les appels WebServices

Commencer par ajouter Retrofit au projet en ajoutant les lignes ci-dessous dans le build.gradle du module

compile 'com.squareup.retrofit:retrofit:1.9.0'

Stocker l’API_KEY

Certains appels de Github sont protégés par une API_KEY (qui doit être envoyée dans les header de la requête). Cette clé sera transmise par Github après l’authentification de l’utilisateur.

Il faut créer une classe Storage qui permet de stocker cette clé dans les SharedPreferences.

L’authentification ne sera pas expliquée dans ce tutoriel, le tutoriel se focalise sur Dagger 2.

Storage.java

/**
 * Classe permettant de stocker l'API KEY dans les SharedPreferences
 */
public class Storage {
    protected final String SHARED_PREFERENCES = "StorageModule";
    protected final String PREFERENCES_API_KEY = "PREFERENCES_API_KEY";

    Context context;

    SharedPreferences sharedPreferences;

    public Storage(Context context) {
        this.context = context;
        this.sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
    }

    public String getApiKey() {
        return this.sharedPreferences.getString(PREFERENCES_API_KEY, null);
    }

    public void setApiKey(String apiKey) {
        this.sharedPreferences.edit().putString(PREFERENCES_API_KEY, apiKey).apply();
    }
}

Il faut maintenant créer une interface de WebService pour Retrofit

GithubService.java

public interface GithubService {

    @GET("/users/{user}/repos")
    void listRepos(@Path("user") String user, Callback<List<Repo>> callback);

}

Retrofit va utiliser cette interface pour nous créer un GithubService. Notez que cette instance aura une instance de Storage comme paramètre afin de pouvoir stocker l’API_KEY

public GithubService createGithubService(final Storage storage) {
        return new RestAdapter.Builder()
                .setEndpoint(BuildConfig.URL_GITHUB)
                .setRequestInterceptor(new RequestInterceptor() {
                    @Override
                    public void intercept(RequestFacade request) {
                        String key = storage.getApiKey();
                        if (key != null) {
                            //ajoute aux header la ApiKey en clé bearer
                            request.addHeader("bearer", key);
                        }
                    }
                })
                .build()
                .create(GithubService.class);
}

Vous voyez facilement le problème :

  1. La création du GithubService nécessite un Storage
  2. La création du Storage nécessite un Context

Donc le GithubService dépend du Storage qui lui même dépend du Context.

Cet exemple est relativement simple afin de vous aider à comprendre le concept des dépendances.

Et ce que nous voulons à tout prix éviter est de réaliser ce travail de dépendances à la main ou de manière statique. Voilà où Dagger2 va intervenir !


Mise en place de Dagger2

Avant de mettre en place Dagger2, il est nécessaire de bien comprendre où il intervient et comment l’utiliser.

Dagger2 a été créé pour principalement remplacer les anciennes utilisée pour créer/stocker les différents Services d’une application. Quand je parle de Service, je parle des outils tels que le stockage, les appels réseau, l’accès à l’appareil photo, ou encore le contexte de l’application.

Dans cet exemple, les services seront :

  • la gestion du Contexte
  • le Stockage
  • les appels du WebService Github

Ils seront ici appelés Dépendances ! (Dependencies)

@Module / @Component

Dagger2 fonctionne avec 2 types d’objets :

  • Module (Factory) : elle contient les méthodes permettant de produire (@Provides) nos dépendances
  • Component (Ce sont nos injecteurs) : On définit les modules utilisés afin de leur fournir les dépendances, puis ils créent automatiquement le graph des dépendances.

@Provide / @Inject

@Inject indique que nous avons besoin d’une dépendance. Plus simplement, il indique à Dagger qu’il faudra construire une instance de la classe annotée.

Dans cet exemple, on indique à Dagger que la classe MainActivity dépend d’une instance de GithubService qui doit être créée et injectée.

public class MainActivity extends AppCompatActivity{

    @Inject GithubService githubService;

    ....
}

@Provides indique que la méthode contenant cette annotation pourra être utilisée par Dagger afin de créer une dépendance.


Implémenter les Modules & Components

Les modules seront annotés @Module et chacune de leur méthode sera annotée @Provides

@Module
public class MonModule {
    @Provides
    public LaDependance provideLaDependance(Context context){
        return new LaDependance(context);
    }
}

Les méthodes du modules sont de la forme

@Provides
public DependanceACreer provideNomDependance(DependanceRequise1 dependance1, DependanceRequise2 dependance2){
     //création & return de la dependance
}

exemple :

/**
 * Ce module permet de récupérer le GithubService et le Storage
 * Dagger2 va les crééer de façon unique (Singleton) et ils seront accéssibles depuis le GithubComponent
 */
@Module
public class GithubModule {

    /**
     * Retourne un Storage à Dagger2, construit avec le Context, injecté par Dagger2
     * @param context Contexte de l'application, fournit par Dagger2
     */
    @Provides
    @Singleton
    public Storage provideStorage(Context context){
        return new Storage(context);
    }

    /**
     * Créé le GithubService via Retrofit, qui réaliser les appels webservices,
     * en utilisant le StorageModule afin de gérer les clés
     *
     * @param storage le Storage injecté par Dagger2 via le StorageModule
     */
    @Singleton
    @Provides
    public GithubService provideGithubService(final Storage storage) {
        return new RestAdapter.Builder()
                .setEndpoint(BuildConfig.URL_GITHUB)
                .setLogLevel(RestAdapter.LogLevel.FULL)
                .setLog(new AndroidLog("Retrofit"))
                .setRequestInterceptor(new RequestInterceptor() {
                    @Override
                    public void intercept(RequestFacade request) {
                        String key = storage.getApiKey();
                        if (key != null) {
                            //ajoute aux header la ApiKey en clé bearer
                            request.addHeader("bearer", key);
                        }
                    }
                })
                .build()
                .create(GithubService.class);
    }
}

La création du Storage nécessite un Context, que nous allons produire grâce à un ContextModule

/**
 * Ce module sert à fournir le contexte de l'application
 */
@Module
public class ContextModule {

    private final Context context;

    /**
     * Nous ajoutons volontairement un constructeur qui prend un Context en entrée,
     * afin de lui fournir au runtime lors de la création de l'Application
     * @param context l'application créée
     */
    public ContextModule(Context context) {
        this.context = context;
    }

    /**
     * Permet à Dagger2 de récupérer le context
     * @return le context de l'application
     */
    @Provides
    Context provideContext() {
        return context;
    }

}

Les Components seront annotés @Component

@Component
public interface MonComponent{
    ...
}

Afin de définir les modules que pourra utiliser un Component, il suffit de les ajouter en tant qu’argument l’annotation du Component :

@Component(modules = {PremierModule.class, DeuxiemeModule.class})
public interface MonComponent{
    ...
}

MonComponent aura alors accès aux @Provide du PremierModule et DeuxiemeModule.

Et le contenu du Component ?

Le component va contenir tout ce dont vous avez besoin du coté “utilisateur” (Activity/Fragment).

@Component(modules = {PremierModule.class, DeuxiemeModule.class})
public interface MonComponent{
    
    DependanceUtilisable nomDependance();

}

Par exemple, si vous avez besoin que MonComponent vous retourne un GithubService, il suffit de lui donner une fonction qui retourne une GithubService. Dagger2 se chargera de le construire avec les modules et les dépendances spécifiés.

@Component(modules = {ContextModule.class,GithubModule.class})
public interface GithubComponent{
    
    GithubService githubService();

}

Afin d’utiliser la commande @Inject directement depuis un fichier source (ex: MainActivity), vous devez ajouter une méthode inject(ClasseSource). Par exemple :

@Component(modules = {ContextModule.class,GithubModule.class})
public interface GithubComponent{
    
    GithubModule githubModule();
    void inject(MainActivity mainActivity);

}

Vous pourrez alors avoir

public class MainActivity extends Activity{

    @Inject
    GithubModule githubService;

    @Override
    public void onCreate(Bundle savedInstanceState){
        ...
    }

}

Construire le GithubComponent

Première étape, définir le Application.class de notre projet :

<application
        android:name=".Application"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

C’est dans cette classe que nous allons définir notre GithubComponent
Application est une classe assez spéciale sous Android, c’est la seule qui est vraiment “Singletonnée”, nous utilisons ici cette spécificité pour stocker notre GithubComponent

public class Application extends android.app.Application {

    protected GithubComponent githubComponent;
    protected static Application application;

    @Override
    public void onCreate() {
        super.onCreate();

        application = this;

        //je créé mon githubcompoent, et le stock dans mon application
        githubComponent = DaggerGithubComponent.builder()
                                .contextModule(new ContextModule(getApplicationContext()))
                                .build();
    }

    public static Application app() {
        return application;
    }

    //permet aux activités via .getApplication().appComponent() de récupérer le AppComponent
    public GithubComponent component() {
        return githubComponent;
    }

}

L’accès à ce dernier se fera de la façon suivante

Application.app().component()

Récupérer la dépendance

Pour utiliser les Components depuis une classe (Activity/Fragment), il faut utiliser la syntaxe suivante (préfixer le nom du component par Dagger) :

GithubComponent component = DaggerGithubComponent.create();

Vous pouvez cependant préciser quel modules utiliser

MonComponent monComponent = DaggerMonComponent.builder()
                .premierModule(new PremierModule())
                .deuxiemeModule(new DeuxiemeModule())
                .build();

Il existe ensuite 2 méthodes pour récupérer les dépendances.

Méthode 1

Si vous avez définit votre Component de la manière suivante

public interface GithubComponent{
    GithubService guthubService();
}

Vous pourrez alors utiliser

public class MainActivity extends Activity{

    GithubService githubService;

    @Override
    public void onCreate(Bundle savedInstanceState){
        ...
        this.githubService = Application.app().component().githubService(); //récupérera le githubService
    }

}

@Inject

Si vous avez définit votre Component de la manière suivante

public interface GithubComponent{
    void inject(MainActivity mainActivity);
}

Vous pourrez alors utiliser

public class MainActivity extends Activity{

    @Inject
    GithubService githubService;

    @Override
    public void onCreate(Bundle savedInstanceState){
        ...
        Application.app().component().inject(this); //injectera le GithubService
    }

}

La premiére partie de ce tutoriel ce termine ici. Dans la seconde partie vous verrez comment utiliser Dagger2 afin de résoudre les dépendances avec le Stockage (Storage) et avec le Service Retrofit.

En attendant n’hésitez pas à poser vos questions ou a nous faire vos remarques

3 commentaires


  1. Joli article! Bravoo. Mais honnetement dagger c’est pas ce prendre la tête pour rien? on peut s’en passer sans des soucis de performances et avec un code plus lisible. Je vois tous l’engouement de la communauté au tour j’ai essayé car je voulais faire bien. Mais je pense que c’est chiant pour rien. Votre avis critique svp?

    Répondre

  2. Article encore une fois très intéressant, belle réussite, continuez comme ça !

    Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *