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 :
-
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.
-
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
-
Installer
AndroidInjectionModule
dans votre composant d’application pour s’assurer que toutes les liaisons nécessaires pour ces types de base sont disponibles. -
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> {}}
-
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 moduleabstract
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();
-
Puis, faites en sorte que votre
Application
implémenteHasAndroidInjector
et@Inject
unDispatchingAndroidInjector<Object>
àretourner de la méthodeandroidInjector()
: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; }}
-
Enfin, dans votre
Activity.onCreate()
méthode, appelezAndroidInjection.inject(this)
avant d’appelersuper.onCreate();
:public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
-
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 Activity
types, injectez les Fragment
s dans onAttach()
.
Contrairement aux modules définis pour les Activity
s, vous avez le choix de l’endroit où installer les modules pour les Fragment
s. 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 Fragment
a 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
etDaggerIntentService
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’éviterNullPointerException
s. 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, DaggerActivity
appelle 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 Activity
doit 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.