L'avenir de l'injection de dépendance dans Android

J'attire votre attention sur une traduction de l' article original de Jamie Sanson
image


Création d'activité avant Android 9 Pie


L'injection de dépendance (DI) est un modèle commun qui est utilisé dans toutes les formes de développement pour un certain nombre de raisons. Grâce au projet Dagger, il est pris comme modèle utilisé dans le développement pour Android. Les récents changements dans Android 9 Pie nous ont fait disposer de plus d'options en matière de DI, en particulier avec la nouvelle classe AppComponentFactory .




DI est très important en matière de développement Android moderne. Cela vous permet de réduire la quantité totale de code lors de l'obtention de liens vers des services utilisés entre les classes, et divise généralement bien l'application en composants. Dans cet article, nous nous concentrerons sur Dagger 2, la bibliothèque DI la plus courante utilisée dans le développement Android. On suppose que vous avez déjà une connaissance de base de la façon dont cela fonctionne, mais il n'est pas nécessaire de comprendre toutes les subtilités. Il est à noter que cet article est un peu une aventure. C'est intéressant et tout, mais au moment de sa rédaction, Android 9 Pie n'apparaissait même pas sur le panneau de version de la plate-forme , donc ce sujet ne sera probablement pas pertinent pour le développement quotidien pendant au moins plusieurs années.


Injection de dépendance dans Android aujourd'hui


En termes simples, nous utilisons DI pour fournir des instances de classes de «dépendance» à nos classes dépendantes, c'est-à-dire celles qui font le travail. Supposons que nous utilisons le modèle de référentiel pour traiter notre logique liée aux données et que nous voulons utiliser notre référentiel dans Activity pour afficher certaines données à l'utilisateur. Nous pourrions vouloir utiliser le même référentiel à plusieurs endroits, nous utilisons donc l'injection de dépendances pour faciliter le partage de la même instance entre un tas de classes différentes.


Tout d'abord, nous fournirons un référentiel. Nous définirons la fonction Provides dans le module, permettant à Dagger de savoir que c'est exactement l'instance que nous voulons implémenter. Veuillez noter que notre référentiel a besoin d'une instance de contexte pour travailler avec des fichiers et le réseau. Nous lui fournirons le contexte de l'application.


 @Module class AppModule(val appContext: Context) { @Provides @ApplicationScope fun provideApplicationContext(): Context = appContext @Provides @ApplicationScope fun provideRepository(context: Context): Repository = Repository(context) } 

Nous devons maintenant définir Component pour gérer l'implémentation des classes dans lesquelles nous voulons utiliser notre Repository .


 @ApplicationScope @Component(modules = [AppModule::class]) interface ApplicationComponent { fun inject(activity: MainActivity) } 

Enfin, nous pouvons configurer notre Activity pour utiliser notre référentiel. Supposons que nous ayons créé une instance de notre ApplicationComponent ailleurs.


 class MainActivity: AppCompatActivity() { @Inject lateinit var repository: Repository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //    application.applicationComponent.inject(this) //       } } 

C'est tout! Nous venons de configurer l'injection de dépendances dans l'application à l'aide de Dagger. Il existe plusieurs façons de procéder, mais cela semble être l'approche la plus simple.


Quel est le problème avec l'approche actuelle?


Dans les exemples ci-dessus, nous avons vu deux types d'injections différents, l'un plus évident que l'autre.


La première que vous avez peut-être manquée est connue sous le nom d' intégration dans le constructeur . Il s'agit d'une méthode de fourniture de dépendances via le constructeur d'une classe, ce qui signifie qu'une classe utilisant des dépendances n'a aucune idée de l'origine des instances. Ceci est considéré comme la forme la plus pure d'injection de dépendances, car elle encapsule parfaitement notre logique d'injection dans nos classes de Module . Dans notre exemple, nous avons utilisé cette approche pour fournir un référentiel:


 fun provideRepository(context: Context): Repository = Repository(context) 

Pour cela, nous avions besoin de Context , que nous avons fourni dans la fonction provideApplicationContext() .


La deuxième chose, plus évidente, que nous avons vue est la mise en œuvre de la classe sur le terrain . Cette méthode a été utilisée dans notre MainActivity pour fournir notre magasin. Ici, nous définissons les champs comme destinataires des injections à l'aide de l'annotation Inject . Ensuite, dans notre fonction onCreate nous indiquons à ApplicationComponent que les dépendances doivent être injectées dans nos champs. Il ne semble pas aussi propre que l'incorporation dans un constructeur, car nous avons une référence explicite à notre composant, ce qui signifie que le concept de l'intégration s'infiltre dans nos classes dépendantes. Un autre défaut dans les classes Android Framework, car nous devons être sûrs que la première chose que nous faisons est de fournir des dépendances. Si cela se produit au mauvais moment du cycle de vie, nous pouvons accidentellement essayer d'utiliser un objet qui n'a pas encore été initialisé.


Idéalement, vous devriez vous débarrasser complètement des implémentations dans les champs de classe. Cette approche ignore les informations d'implémentation pour les classes qui n'ont pas besoin de les connaître et peuvent potentiellement provoquer des problèmes de cycle de vie. Nous avons vu des tentatives pour le faire mieux, et Dagger sur Android est un moyen assez fiable, mais à la fin, il serait préférable d'utiliser simplement l'incorporation dans le constructeur. Actuellement, nous ne pouvons pas utiliser cette approche pour un certain nombre de classes d'infrastructure, telles que «Activité», «Service», «Application», etc., car elles sont créées pour nous par le système. Il semble que pour le moment, nous ne pouvons pas introduire de cours dans les domaines. Néanmoins, Android 9 Pie prépare quelque chose d'intéressant, qui, peut-être, changera fondamentalement tout.


Injection de dépendances dans Android 9 Pie


Comme mentionné au début de l'article, Android 9 Pie a une classe AppComponentFactory. La documentation est plutôt rare et est simplement affichée sur le site Web du développeur en tant que telle:


L'interface utilisée pour contrôler la création d'éléments manifestes.

C'est intrigant. Les «éléments du manifeste» se réfèrent ici aux classes que nous AndroidManifest dans notre fichier AndroidManifest - telles que l'activité, le service et notre classe d'application. Cela nous permet de "contrôler la création" de ces éléments ... alors, hé, pouvons-nous maintenant fixer les règles de création de nos activités? Quel délice!


Creusons plus profondément. Nous allons commencer par étendre AppComponentFactory et AppComponentFactory la méthode AppComponentFactory .


 class InjectionComponentFactory: AppComponentFactory() { private val repository = NonContextRepository() override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return when { className == MainActivity::class.java.name -> MainActivity(repository) else -> super.instantiateActivity(cl, className, intent) } } } 

Nous devons maintenant déclarer notre fabrique de composants dans le manifeste à l'intérieur de la balise d' application .


 <application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name=".InjectionApp" android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory" android:theme="@style/AppTheme" tools:replace="android:appComponentFactory"> 

Enfin, nous pouvons lancer notre application ... et ça marche! Notre NonContextRepository fourni via le constructeur MainActivity. Avec grâce!


Veuillez noter qu'il y a des réserves. Nous ne pouvons pas utiliser Context ici, car avant même son existence, un appel à notre fonction se produit - c'est déroutant! Nous pouvons aller plus loin pour que le constructeur implémente notre classe Application, mais voyons comment Dagger peut rendre cela encore plus facile.


Rencontre - Dagger Multi-Binds


Je n'entrerai pas dans les détails de l'opération de reliure multiple Dagger sous le capot, car cela dépasse le cadre de cet article. Tout ce que vous devez savoir, c'est qu'il fournit un bon moyen d'injecter dans le constructeur de classe sans avoir à appeler manuellement le constructeur. Nous pouvons l'utiliser pour implémenter facilement des classes de framework d'une manière évolutive. Voyons comment tout cela s'additionne.


Commençons par configurer notre activité pour déterminer où aller ensuite.


 class MainActivity @Inject constructor( private val repository: NonContextRepository ): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //       } } 

Cela montre immédiatement qu'il n'y a presque aucune mention de l'injection de dépendance. La seule chose que nous voyons est l'annotation Inject devant le constructeur.


Vous devez maintenant changer le composant et le module Dagger:


 @Component(modules = [ApplicationModule::class]) interface ApplicationComponent { fun inject(factory: InjectionComponentFactory) } 

 @Module(includes = [ComponentModule::class]) class ApplicationModule { @Provides fun provideRepository(): NonContextRepository = NonContextRepository() } 

Rien n'a beaucoup changé. Il ne nous reste plus qu'à implémenter notre fabrique de composants, mais comment créer nos éléments manifestes? Ici, nous avons besoin d'un ComponentModule . Voyons voir:


 @Module abstract class ComponentModule { @Binds @IntoMap @ComponentKey(MainActivity::class) abstract fun bindMainActivity(activity: MainActivity): Any @Binds abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey internal annotation class ComponentKey(val clazz: KClass<out Any>) 

Ouais, eh bien, juste quelques annotations. Ici, nous associons notre Activity à une carte, implémentons cette carte dans notre classe ComponentHelper et fournissons cette ComponentHelper - le tout en deux instructions Binds . Dagger sait comment instancier notre MainActivity grâce à l'annotation MainActivity Inject afin qu'il puisse «lier» le fournisseur à cette classe, fournissant automatiquement les dépendances de constructeur dont nous avons besoin. Notre ComponentHelper le suivant.


 class ComponentHelper @Inject constructor( private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>> ): ComponentInstanceHelper { @Suppress("UNCHECKED_CAST") override fun <T> resolve(className: String): T? = creators .filter { it.key.name == className } .values .firstOrNull() ?.get() as? T } interface InstanceComponentHelper { fun <T> resolve(className: String): T? } 

Autrement dit, nous avons maintenant une carte des classes pour les fournisseurs de ces classes. Lorsque nous essayons de résoudre une classe par son nom, nous trouvons simplement le fournisseur de cette classe (si nous en avons un), l'appelons pour obtenir une nouvelle instance de cette classe et la renvoyons.


Enfin, nous devons apporter des modifications à notre AppComponentFactory pour utiliser notre nouvelle classe d'assistance.


 class InjectionComponentFactory: AppComponentFactory() { @Inject lateinit var componentHelper: ComponentInstanceHelper init { DaggerApplicationComponent.create().inject(this) } override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return componentHelper .resolve<Activity>(className) ?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent) } } 

Exécutez à nouveau le code. Tout fonctionne! Quel délice.


Problèmes d'implémentation du constructeur


Un tel titre peut ne pas sembler très impressionnant. Bien que nous puissions incorporer la plupart des instances en mode normal en les injectant dans le constructeur, nous n'avons aucun moyen évident de fournir un contexte pour nos dépendances de manière standard. Mais le Context dans Android est tout. Il est nécessaire pour accéder aux paramètres, au réseau, à la configuration des applications et bien plus encore. Nos dépendances sont souvent des choses qui utilisent des services liés aux données, tels que le réseau et les paramètres. Nous pouvons contourner cela en réécrivant nos dépendances pour en faire de pures fonctions, ou en initialisant tout avec des instances de contexte dans notre classe Application , mais il faut beaucoup plus de travail pour déterminer la meilleure façon de le faire.


Un autre inconvénient de cette approche est la définition de la portée. Dans Dagger, l'un des concepts clés pour implémenter l'injection de dépendances hautes performances avec une bonne séparation des relations de classe est la modularité du graphe d'objet et l'utilisation de la portée. Bien que cette approche n'interdise pas l'utilisation de modules, elle limite l'utilisation de la portée. AppComponentFactory existe à un niveau d'abstraction complètement différent par rapport à nos classes de framework standard - nous ne pouvons pas obtenir de lien vers celui-ci par programme, nous n'avons donc aucun moyen de lui demander de fournir des dépendances pour Activity dans une portée différente.


Il existe de nombreuses façons de résoudre nos problèmes avec les étendues dans la pratique, dont l'une consiste à utiliser une FragmentFactory pour intégrer nos fragments dans un constructeur avec des étendues. Je n'entrerai pas dans les détails, mais il s'avère que nous avons maintenant une méthode pour contrôler la création de fragments, qui non seulement nous donne beaucoup plus de liberté en termes de portée, mais a également une compatibilité descendante.


Conclusion


Android 9 Pie a introduit un moyen d'utiliser l'incorporation dans le constructeur pour fournir des dépendances dans nos classes de framework, telles que «Activity» et «Application». Nous avons vu qu'avec Dagger Multi-binding, nous pouvons facilement fournir des dépendances au niveau de l'application.


Un constructeur qui implémente tous nos composants est extrêmement attrayant, et nous pouvons même faire quelque chose pour le faire fonctionner correctement avec des instances de contexte. C'est un avenir prometteur, mais il n'est disponible qu'à partir de l'API 28. Si vous souhaitez toucher moins de 0,5% des utilisateurs, vous pouvez l'essayer. Sinon, vous devriez attendre et voir si une telle méthode reste pertinente dans quelques années.

Source: https://habr.com/ru/post/fr444530/


All Articles