Introduction à Dagger2 – Partie 1

Introduction

Reprenons l’exemple de base 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

Commençons par ajouter Retrofit à notre projet, en ajoutant cette entrée dans notre build.gradle

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

Je vais vous donner une astuce pour vous passer des fichiers de constantes, souvent inutiles, ou variables en fonction des options de compilations (flavors).
Avec Gradle, nous pouvons par exemple générer une version de production, reliée au vrai serveur de notre entreprise, puis une version dite pré-production, reliée à un serveur contenant de fausses données. La création de ces flavors fera l’objet d’un futur tutoriel, je vais cependant vous montrer comment gérer nos constantes :

Il suffit d’ajouter aux buildTypes du fichier build.gradle des buildConfigField

Dans notre cas, nous ajoutons un String, nommé URL_GITHUB dont la valeur est “https://api.github.com” (penser à échapper les double-quotes (“) en mettant un back-slash (\) devant).

build.gradle

buildTypes {
        debug{
            buildConfigField "String", "URL_GITHUB", "\"https://api.github.com\""
        }
        release {
            buildConfigField "String", "URL_GITHUB", "\"https://api.github.com\""

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

Ce qui va générer un fichier BuildConfig.java, contenant notre champ URL_GITHUB

public final class BuildConfig {
    public static final boolean DEBUG = Boolean.parseBoolean("true");
    public static final String APPLICATION_ID = "com.tutosandroidfrance.dagger2sample";
    public static final String BUILD_TYPE = "debug";
    public static final String FLAVOR = "";
    public static final int VERSION_CODE = 1;
    public static final String VERSION_NAME = "1.0";
    // Fields from build type: debug
    public static final String URL_GITHUB = "https://api.github.com";
}

Nous pourrons donc utiliser dans notre code BuildConfig.URL_GITHUB afin de récupérer https://api.github.com.
Pour les afficionados de ProductFlavor, cela peut fournir une url différente pour une version PreProd.

Stocker l’API_KEY

Certains appels de Github sont protégés par une API_KEY. Cette clé doit être envoyée dans les headers de notre requête. Elle sera envoyée par Github après l’ authentification de l’utilisateur via Username et Password.

C’est pourquoi nous avons besoin d’une classe permettant de stocker cette clé, nous créons donc une classe Storage, qui va sauvegarder l’API_KEY dans les SharedPreferences.

Ce tutoriel étant axé sur Dagger2, nous ne gérerons pas l’authentification ici. L’objet Storage sera juste utilisé par Dagger et à aucun moment nous ne ferons la demande de l’API_KEY à Github.

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

Puis à notre habitude, nous allons créer notre 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 nous allons lui fournir une instance de Storage afin qu’il puisse ajouter l’API_KEY dans les headers de chaque requête

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 notre problème :

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

J’ai choisi un exemple relativement simple afin de vous aider à comprendre le concept des dépendances.

Ce que nous voulons à tout prix éviter, est de réaliser ce travail de dépendances à la main, ou de créer plusieurs Storage/GithubService. Voilà où Dagger2 va intervenir !


Dagger2

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 repris par Google : http://google.github.io/dagger/

Importer Dagger2

Dagger fonctionne principalement avec des annotations.
Nous aurons aussi besoin d’importer le module Gradle android-apt, permettant une meilleure gestion des annotations sur Android-Studio.

Android-apt est un plugin Gralde. La dépendance com.neenbedankt.gradle.plugins:android-apt est donc à ajouter dans le /build.gradle du 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'
    }
}

Ensuite, il faut ajouter l’instruction apply plugin: ‘com.neenbedankt.android-apt’ en haut du /build.gradle du module.
Ceci permet d’utiliser l’instruction apt (qui agit de la même manière que compile).

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

Nous ajoutons aussi la librairie javax.annotations, contenant entre autres les annotations @Inject et @Provides.


Introduction à 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 remplacer les anciennes FactoryWrapper ou même Singleton.
Nous les utilisons afin de créer/stocker les différents Services de notre application. Quand je parle de Services, 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 notre exemple, les services seront :

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

Ces services seront ici appelés Dépendances ! (Dependencies)

@Module / @Component

Dagger2 fonctionne avec 2 types d’objet :

  • Module : C’est le Factory. Il contient les méthodes pour de créer nos dépendances (@Provides).
  • Component : C’est notre injecteur. On définit les modules qu’il va utiliser afin de lui fournir les dépendances. Ensuite il crée tout seul le graphe des dépendances afin de nous fournir nos services.

@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.
exemple:

public class MainActivity extends AppCompatActivity{

    @Inject GithubService githubService;

    ....
}

@Provides est une annotation utilisée sur les méthodes d’un module. Elle indique à Dagger que la méthode annotée servira à créer une dépendance.


Ecrire nos Modules & Components

Un module est annoté @Module.
Comme expliqué précédemment, chaque méthode du module est annotée @Provides.

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

Les méthodes du module sont de la forme

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

Un Component est annoté @Component

@Component
public interface MonComponent{
    ...
}

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

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

MonComponent aura alors accès aux méthodes de PremierModule et DeuxiemeModule.

Il est aussi possible de créer des dépendances entre les Components.
En effet, si un Component a besoin d’une dépendance qu’il ne peut créer, il peut demander à un deuxième Component de lui fournir cette dépendance.
Imaginons que MonComponent ait besoin d’un Bitmap mais qu’aucun de ses modules ne sait le créer. Nous pouvons alors lui ajouter une dépendance vers ImageComponent, qui sait créer un Bitmap :

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

Et le contenu du Component ?

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

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

}

Par exemple, si vous avez besoin que MonComponent vous retourne un Bitmap, il suffit de lui donner une méthode qui retourne un Bitmap. Dagger2 se chargera de vous le construire avec les modules et les dépendances que vous lui enverrez.

@Component(dependencies = {ImageComponent.class}, modules = {PremierModule.class, DeuxiemeModule.class})
public interface MonComponent{
    
    Bitmap bitmap();

}

Afin d’utiliser l’annotation @Inject directement depuis votre source (ex: MainActivity), vous devez ajouter une méthode inject(ClasseSource), exemple :

@Component(dependencies = {ImageComponent.class}, modules = {PremierModule.class, DeuxiemeModule.class})
public interface MonComponent{
    
    Bitmap bitmap();
    void inject(MainActivity mainActivity);

}

Vous pourrez alors avoir

public class MainActivity extends Activity{

    @Inject
    Bitmap bitmap;

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

}

Récupérer la dépendance

Pour utiliser les Components depuis votre classe (Activity/Fragment), il faudra utiliser la syntaxe suivante

MonComponent component = DaggerMonComponent.builder().build();

Vous pouvez ensuite préciser quel Component utiliser en dépendance, ainsi que la liste des modules qu’il devra utiliser afin de construire ses dépendances (ceux définis en tant qu’argument du @Component):

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

La précision de ces modules est optionnelle. S’ils ne sont pas spécifiés, Dagger2 utilisera les modules par défaut. Cela permet par exemple de remplacer un module de stockage afin qu’il stocke dans une base de données et non dans les SharedPreferences.

Vous pourrez ensuite récupérer votre dépendance de 2 façons

Méthode

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

public interface MonComponent{
    Bitmap bitmap();
}

Vous pourrez alors utiliser

public class MainActivity extends Activity{

    Bitmap bitmap;

    @Override
    public void onCreate(Bundle savedInstanceState){
        MonComponent monComponent = DaggerMonComponent.builder()
                .maDependanceComponent(dependanceComponent)
                .premierModule(new PremierModule())
                .deuxiemeModule(new DeuxiemeModule())
                .build();

        this.bitmap = monComponent.bitmap(); //récupérera le bitmap
    }

}

@Inject

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

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

Vous pourrez alors utiliser

public class MainActivity extends Activity{

    @Inject
    Bitmap bitmap;

    @Override
    public void onCreate(Bundle savedInstanceState){
        MonComponent monComponent = DaggerMonComponent.builder()
                .maDependanceComponent(dependanceComponent)
                .premierModule(new PremierModule())
                .deuxiemeModule(new DeuxiemeModule())
                .build();

        monComponent.inject(this); //injectera le bitmap
    }

}

Ce tutoriel est découpé en 2 parties, dans la seconde vous verrez comment utiliser Dagger2 afin de résoudre le problème de dépendance avec Retrofit et Stockage vu au début de ce tuto.

Laisser un commentaire

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