Dagger & Android

L’un des principaux avantages de Dagger 2 par rapport à la plupart des autres cadres d’injection de dépendances est que son implémentation strictement générée (sans réflexion) signifie qu’il peut être utilisé dans les applications Android. Cependant, il y a encore quelques considérations à faire lors de l’utilisation de Dagger dans les applications Android.

Philosophie

Bien que le code écrit pour Android soit source Java, il est souvent assez différent interms of style. Typiquement, de telles différences existent pour s’adapter aux considérations uniques de performance d’une plate-forme mobile.

Mais beaucoup des modèles communément appliqués au code destiné à Android sontcontraires à ceux appliqués à d’autres codes Java. Même une grande partie des conseils dansEffective Java est considérée comme inappropriée pour Android.

Afin d’atteindre les objectifs d’un code à la fois idiomatique et portable, Daggerrepose sur ProGuard pour post-traiter le bytecode compilé. Cela permet à Dagger d’émettre une source qui semble naturelle sur le serveur et Android, tout en utilisant les différentes chaînes d’outils pour produire un bytecode qui s’exécute efficacement dans les deux environnements. De plus, Dagger a pour objectif explicite de s’assurer que la source Java qu’il génère est systématiquement compatible avec les optimisationsProGuard.

Bien sûr, tous les problèmes ne peuvent pas être traités de cette manière, mais c’est le principal mécanisme par lequel la compatibilité spécifique à Android sera fournie.

tl;dr

Dagger suppose que les utilisateurs sur Android utiliseront R8 ou ProGuard.

Pourquoi Dagger sur Android est difficile

L’une des difficultés centrales de l’écriture d’une application Android en utilisant Dagger est que de nombreuses classes du framework Android sont instanciées par l’OS lui-même, commeActivity et Fragment, mais Dagger fonctionne mieux s’il peut créer tous les objetsinjectés. Au lieu de cela, vous devez effectuer l’injection de membres dans une méthode de cycle de vie. Cela signifie que de nombreuses classes finissent par ressembler à :

public class FrombulationActivity extends Activity { @Inject Frombulator frombulator; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // DO THIS FIRST. Otherwise frombulator might be null! ((SomeApplicationBaseType) getContext().getApplicationContext()) .getApplicationComponent() .newActivityComponentBuilder() .activity(this) .build() .inject(this); // ... now you can write the exciting code }}

Cela pose quelques problèmes :

  1. Le copier-coller de code rend difficile le remaniement ultérieur. Comme de plus en plus de développeurs copient-collent ce bloc, moins nombreux seront ceux qui sauront ce qu’il fait réellement.

  2. Plus fondamentalement, cela nécessite que le type demandant l’injection(FrombulationActivity) connaisse son injecteur. Même si cela est fait par le biais d’interfaces au lieu de types concrets, cela brise un principe de base de l’injection de dépendance : une classe ne devrait rien savoir de la façon dont elle est injectée.

dagger.android

Les classes dans dagger.android offrent une approche pour simplifier les problèmes ci-dessus. Cela nécessite l’apprentissage de quelques API et concepts supplémentaires mais vous donne un boilerplate réduit et une injection dans vos classes Android au bon endroit dans le cycle de vie.

Une autre approche consiste à simplement utiliser les API normales de Dagger et à suivre des guides tels que celui ici.Cela peut être plus simple à comprendre, mais vient avec l’inconvénient d’avoir à écrireextra boilerplate manuellement.

Les équipes Jetpack et Dagger travaillent ensemble sur une nouvelle initiative pour Dagger sur Android qui espère être un grand changement par rapport au statusquo actuel. Bien qu’il ne soit malheureusement pas encore prêt, cela peut être quelque chose à considérerlorsque vous choisissez comment utiliser Dagger dans vos projets Android aujourd’hui.

Injecter des objets Activity

  1. Installer AndroidInjectionModule dans votre composant d’application pour s’assurer que toutes les liaisons nécessaires pour ces types de base sont disponibles.

  2. Commencez par écrire un @Subcomponent qui implémenteAndroidInjector<YourActivity>, avec un@Subcomponent.Factory qui étendAndroidInjector.Factory<YourActivity> :

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. Après avoir défini le sous-composant, ajoutez-le à votre hiérarchie de composants endéfinissant un module qui lie la fabrique du sous-composant et en l’ajoutant aucomposant qui injecte votre Application :

    @Module(subcomponents = YourActivitySubcomponent.class)abstract class YourActivityModule { @Binds @IntoMap @ClassKey(YourActivity.class) abstract AndroidInjector.Factory<?> bindYourAndroidInjectorFactory(YourActivitySubcomponent.Factory factory);}@Component(modules = {..., YourActivityModule.class})interface YourApplicationComponent { void inject(YourApplication application);}

    Pro-tip : Si votre sous-composant et sa fabrique n’ont pas d’autres méthodes ousupertypes que ceux mentionnés dans l’étape #2, vous pouvez utiliser@ContributesAndroidInjector pour les générer pour vous. Au lieu des étapes 2et 3, ajoutez une méthode de module abstract qui renvoie votre activité, annotez-la avec @ContributesAndroidInjector, et spécifiez les modules que vous voulezinstaller dans le sous-composant. Si le sous-composant a besoin de scopes, appliquez également les annotations de scopes à la méthode.

    @ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
  4. Puis, faites en sorte que votre Application implémente HasAndroidInjectoret @Inject unDispatchingAndroidInjector<Object> àretourner de la méthode androidInjector() :

    public class YourApplication extends Application implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector; @Override public void onCreate() { super.onCreate(); DaggerYourApplicationComponent.create() .inject(this); } @Override public AndroidInjector<Object> androidInjector() { return dispatchingAndroidInjector; }}
  5. Enfin, dans votre Activity.onCreate() méthode, appelezAndroidInjection.inject(this)avant d’appeler super.onCreate();:

    public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
  6. Congratulations !

Comment ça a marché ?

AndroidInjection.inject() obtient un DispatchingAndroidInjector<Object> du Application et passe votre activité à inject(Activity). LeDispatchingAndroidInjector recherche le AndroidInjector.Factory pour la classe de votre activité (qui est YourActivitySubcomponent.Factory), crée leAndroidInjector (qui est YourActivitySubcomponent), et passe votre activité à inject(YourActivity).

Injecter des objets Fragment

Injecter un Fragment est aussi simple que d’injecter un Activity. Définissez votre sous-composant de la même manière.

Au lieu d’injecter dans onCreate() comme cela se fait pour les Activitytypes, injectez les Fragments dans onAttach().

Contrairement aux modules définis pour les Activitys, vous avez le choix de l’endroit où installer les modules pour les Fragments. Vous pouvez faire de votre composant Fragment un sous-composant d’un autre composant Fragment, un composant Activity, ou le composantApplication – tout dépend des autres liaisons dont votre Fragment a besoin. Après avoir décidé de l’emplacement du composant, faites le typeimplement HasAndroidInjector correspondant (s’il ne le fait pas déjà). Par exemple, si votre Fragmenta besoin de liaisons à partir de YourActivitySubcomponent, votre code ressemblera à quelque chose comme ceci:

public class YourActivity extends Activity implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> androidInjector; @Override public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); // ... } @Override public AndroidInjector<Object> androidInjector() { return androidInjector; }}public class YourFragment extends Fragment { @Inject SomeDependency someDep; @Override public void onAttach(Activity activity) { AndroidInjection.inject(this); super.onAttach(activity); // ... }}@Subcomponent(modules = ...)public interface YourFragmentSubcomponent extends AndroidInjector<YourFragment> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourFragment> {}}@Module(subcomponents = YourFragmentSubcomponent.class)abstract class YourFragmentModule { @Binds @IntoMap @ClassKey(YourFragment.class) abstract AndroidInjector.Factory<?> bindYourFragmentInjectorFactory(YourFragmentSubcomponent.Factory factory);}@Subcomponent(modules = { YourFragmentModule.class, ... }public interface YourActivityOrYourApplicationComponent { ... }

Types de base du cadre

Parce que DispatchingAndroidInjector recherche laAndroidInjector.Factory appropriée par la classe au moment de l’exécution, une classe de base peut implémenterHasAndroidInjector ainsi qu’appeler AndroidInjection.inject(). Tout ce que chaque sous-classe doit faire est de lier un @Subcomponent correspondant. Dagger fournit quelques types de base qui font cela, comme DaggerActivity et DaggerFragment, si vous n’avez pas une hiérarchie de classe compliquée. Dagger fournit également un DaggerApplication dans le même but – tout ce que vous devez faire est de l’étendre et de surcharger la méthode applicationInjector() pour retourner le composant qui doit injecter le Application.

Les types suivants sont également inclus:

  • DaggerService et DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

Note : DaggerBroadcastReceiver ne doit être utilisé que lorsque leBroadcastReceiver est enregistré dans le AndroidManifest.xml. Lorsque leBroadcastReceiver est créé dans votre propre code, préférez l’injection de constructeur à la place.

Bibliothèques de support

Pour les utilisateurs de la bibliothèque de support Android, des types parallèles existent dans le paquetdagger.android.support.

TODO(ronshapiro) : nous devrions commencer à diviser cela par paquets androidx

Comment puis-je l’obtenir ?

Ajoutez ce qui suit à votre build.gradle:

dependencies { implementation 'com.google.dagger:dagger-android:2.x' implementation 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries annotationProcessor 'com.google.dagger:dagger-android-processor:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x'}

Quand injecter

L’injection de constructeur est préférée chaque fois que possible parce que javac elle garantira qu’aucun champ n’est référencé avant d’avoir été défini, ce qui permet d’éviterNullPointerExceptions. Lorsque l’injection des membres est nécessaire (comme discuté ci-dessus), préférez l’injection le plus tôt possible. Pour cette raison, DaggerActivityappelle AndroidInjection.inject() immédiatement dans onCreate(), avant d’appelersuper.onCreate(), et DaggerFragment fait de même dans onAttach(), ce qui évite également les incohérences si le Fragment est rattaché.

Il est crucial d’appeler AndroidInjection.inject() avant super.onCreate() dans un Activity, car l’appel à super attache les Fragment de l’instance d’activité précédente pendant le changement de configuration, qui à son tour injecte les Fragment. Pour que l’injection de Fragment réussisse, le Activitydoit déjà être injecté. Pour les utilisateurs de ErrorProne, c’est une erreur de compilation d’appeler AndroidInjection.inject() après super.onCreate().

FAQ

Scoping AndroidInjector.Factory

AndroidInjector.Factory est destiné à être une interface sans état afin que les implémenteurs n’aient pas à se soucier de la gestion de l’état lié à l’objet qui sera injecté. Lorsque DispatchingAndroidInjector demande uneAndroidInjector.Factory, elle le fait par le biais d’une Provider de sorte qu’elle ne conserve pas explicitement d’instances de la fabrique. Parce que certaines implémentations peuvent retenir une instance de la Activity/Fragment/etc qui est injectée, c’est une erreur de compilation d’appliquer une portée aux méthodes qui les fournissent. Si vous êtes certain que votre AndroidInjector.Factory ne retient pas une instance de l’objet injecté, vous pouvez supprimer cette erreur en appliquant@SuppressWarnings("dagger.android.ScopedInjectorFactory") à votre méthode de module.