El futuro de la inyección de dependencia en Android

Les traigo una traducción del artículo original de Jamie Sanson.
imagen


Crear actividad antes de Android 9 Pie


La inyección de dependencia (DI) es un modelo común que se utiliza en todas las formas de desarrollo por varias razones. Gracias al proyecto Dagger, se toma como una plantilla utilizada en el desarrollo para Android. Los cambios recientes en Android 9 Pie nos han hecho tener más opciones cuando se trata de DI, especialmente con la nueva clase AppComponentFactory .




DI es muy importante cuando se trata del desarrollo moderno de Android. Esto le permite reducir la cantidad total de código al obtener enlaces a servicios utilizados entre clases, y generalmente divide bien la aplicación en componentes. En este artículo, nos centraremos en Dagger 2, la biblioteca DI más común utilizada en el desarrollo de Android. Se supone que ya tiene un conocimiento básico de cómo funciona esto, pero no es necesario comprender todas las sutilezas. Vale la pena señalar que este artículo es un poco una aventura. Esto es interesante y todo, pero en el momento de su redacción, Android 9 Pie ni siquiera apareció en el panel de versión de la plataforma , por lo que este tema probablemente no será relevante para el desarrollo diario durante al menos varios años.


Inyección de dependencia en Android hoy


En pocas palabras, utilizamos DI para proporcionar instancias de clases de "dependencia" a nuestras clases dependientes, es decir, aquellas que hacen el trabajo. Digamos que usamos el patrón de Repositorio para procesar nuestra lógica relacionada con los datos, y queremos usar nuestro repositorio en Actividad para mostrar algunos datos al usuario. Es posible que deseemos usar el mismo repositorio en varios lugares, por lo que usamos la inyección de dependencia para que sea más fácil compartir la misma instancia entre un grupo de clases diferentes.


Primero, proporcionaremos un repositorio. Definiremos la función Provides en el módulo, permitiendo a Dagger saber que esta es exactamente la instancia que queremos implementar. Tenga en cuenta que nuestro repositorio necesita una instancia de contexto para trabajar con archivos y la red. Le proporcionaremos el contexto de la aplicación.


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

Ahora necesitamos definir Component para manejar la implementación de las clases en las que queremos usar nuestro Repository .


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

Finalmente, podemos configurar nuestra Activity para usar nuestro repositorio. Supongamos que creamos una instancia de nuestro Componente de ApplicationComponent en otro lugar.


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

Eso es todo! Acabamos de configurar la inyección de dependencia dentro de la aplicación usando Dagger. Hay varias formas de hacer esto, pero este parece ser el enfoque más fácil.


¿Qué tiene de malo el enfoque actual?


En los ejemplos anteriores, vimos dos tipos diferentes de inyecciones, una más obvia que la otra.


Lo primero que puede haber perdido se conoce como incrustar en el constructor . Este es un método para proporcionar dependencias a través del constructor de una clase, lo que significa que una clase que usa dependencias no tiene idea del origen de las instancias. Esta se considera la forma más pura de inyección de dependencia, ya que encapsula nuestra lógica de inyección en nuestras clases de Module perfectamente. En nuestro ejemplo, utilizamos este enfoque para proporcionar un repositorio:


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

Para esto necesitábamos Context , que proporcionamos en la función provideApplicationContext() .


La segunda cosa más obvia que vimos es la implementación de la clase en el campo . Este método se utilizó en nuestra MainActivity para proporcionar nuestra tienda. Aquí definimos los campos como receptores de las inyecciones usando la anotación Inject . Luego, en nuestra función onCreate le decimos a ApplicationComponent que las dependencias deben inyectarse en nuestros campos. No se ve tan limpio como incrustar en un constructor, porque tenemos una referencia explícita a nuestro componente, lo que significa que el concepto de incrustación se está filtrando en nuestras clases dependientes. Otro defecto en las clases de Android Framework, ya que debemos estar seguros de que lo primero que hacemos es proporcionar dependencias. Si esto sucede en el punto incorrecto del ciclo de vida, podemos intentar accidentalmente usar un objeto que aún no se ha inicializado.


Idealmente, debería deshacerse por completo de las implementaciones en los campos de clase. Este enfoque omite la información de implementación para las clases que no necesitan saber al respecto y que potencialmente pueden causar problemas en el ciclo de vida. Vimos intentos de hacerlo mejor, y Dagger en Android es una forma bastante confiable, pero al final sería mejor si pudiéramos usar la incrustación en el constructor. Actualmente, no podemos utilizar este enfoque para una serie de clases de marco, como "Actividad", "Servicio", "Aplicación", etc., ya que el sistema las crea para nosotros. Parece que en este momento estamos atascados en la introducción de clases en los campos. Sin embargo, Android 9 Pie está preparando algo interesante que, tal vez, cambiará fundamentalmente todo.


Inyección de dependencia en Android 9 Pie


Como se mencionó al principio del artículo, Android 9 Pie tiene una clase AppComponentFactory. La documentación es bastante escasa, y simplemente se publica en el sitio web del desarrollador como tal:


La interfaz utilizada para controlar la creación de elementos manifiestos.

Es intrigante. Los "elementos de manifiesto" aquí se refieren a las clases que enumeramos en nuestro archivo de AndroidManifest , como Actividad, Servicio y nuestra clase de Aplicación. Esto nos permite "controlar la creación" de estos elementos ... así que, ¿podemos establecer las reglas para crear nuestras actividades? ¡Qué delicia!


Vamos a cavar más profundo. Comenzaremos extendiendo AppComponentFactory y reemplazando el método instantiateActivity .


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

Ahora necesitamos declarar nuestra fábrica de componentes en el manifiesto dentro de la etiqueta de la aplicación .


 <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"> 

Finalmente, podemos lanzar nuestra aplicación ... ¡y funciona! Nuestro NonContextRepository proporciona a través del constructor MainActivity. Con gracia!


Tenga en cuenta que hay algunas reservas. No podemos usar Context aquí, ya que incluso antes de su existencia, se produce una llamada a nuestra función, ¡esto es confuso! Podemos ir más allá para que el constructor implemente nuestra clase de Aplicación, pero veamos cómo Dagger puede hacer esto aún más fácil.


Meet - Dagger Multi-Binds


No entraré en los detalles de la operación de enlace múltiple Dagger debajo del capó, ya que esto está más allá del alcance de este artículo. Todo lo que necesita saber es que proporciona una buena manera de inyectarse en el constructor de la clase sin tener que llamar manualmente al constructor. Podemos usar esto para implementar fácilmente clases de marco de una manera escalable. Veamos cómo se suma todo.


Configuremos nuestra actividad primero para averiguar a dónde ir después.


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

Esto muestra inmediatamente que casi no se menciona la inyección de dependencia. Lo único que vemos es la anotación Inject antes del constructor.


Ahora necesita cambiar el componente y el módulo Dagger:


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

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

Nada ha cambiado mucho. Ahora solo necesitamos implementar nuestra fábrica de componentes, pero ¿cómo creamos nuestros elementos manifiestos? Aquí necesitamos un ComponentModule . A ver:


 @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>) 

Sí, bueno, solo unas pocas anotaciones. Aquí conectamos nuestra Activity con un mapa, implementamos este mapa en nuestra clase ComponentHelper y proporcionamos este ComponentHelper , todo en dos instrucciones de Binds . Dagger sabe cómo instanciar nuestra MainActivity gracias a la anotación MainActivity Inject por lo que puede "vincular" al proveedor a esta clase, proporcionando automáticamente las dependencias que necesitamos para el constructor. Nuestro ComponentHelper siguiente.


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

En pocas palabras, ahora tenemos un mapa de clase para proveedores para estas clases. Cuando intentamos resolver una clase por nombre, simplemente encontramos el proveedor para esta clase (si tenemos una), la llamamos para obtener una nueva instancia de esta clase y la devolvemos.


Finalmente, necesitamos hacer cambios en nuestra AppComponentFactory para usar nuestra nueva clase auxiliar.


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

Ejecute el código nuevamente. ¡Todo funciona! Que delicia.


Problemas de implementación del constructor


Tal título puede no parecer muy impresionante. Aunque podemos incrustar la mayoría de las instancias en modo normal inyectándolas en el constructor, no tenemos una forma obvia de proporcionar contexto para nuestras dependencias de manera estándar. Pero el Context en Android es todo. Es necesario para acceder a la configuración, la red, la configuración de la aplicación y mucho más. Nuestras dependencias son a menudo cosas que utilizan servicios relacionados con datos, como la red y la configuración. Podemos solucionar esto reescribiendo nuestras dependencias para que sean funciones puras o inicializando todo con instancias de contexto en nuestra clase de Application , pero se necesita mucho más trabajo para determinar la mejor manera de hacerlo.


Otra desventaja de este enfoque es la definición de alcance. En Dagger, uno de los conceptos clave para implementar la inyección de dependencia de alto rendimiento con una buena separación de las relaciones de clase es la modularidad del gráfico de objetos y el uso del alcance. Aunque este enfoque no prohíbe el uso de módulos, limita el uso del alcance. AppComponentFactory existe en un nivel de abstracción completamente diferente en relación con nuestras clases de marco estándar: no podemos obtener un enlace mediante programación, por lo que no tenemos forma de indicarle que proporcione dependencias para Activity en un ámbito diferente.


Hay muchas maneras de resolver nuestros problemas con los ámbitos en la práctica, una de las cuales es utilizar una FragmentFactory para incrustar nuestros fragmentos en un constructor con ámbitos. No entraré en detalles, pero resulta que ahora tenemos un método para gestionar la creación de fragmentos, que no solo nos da mucha más libertad en términos de alcance, sino que también tiene compatibilidad con versiones anteriores.


Conclusión


Android 9 Pie introdujo una forma de utilizar la incrustación en el constructor para proporcionar dependencias en nuestras clases de marco, como "Actividad" y "Aplicación". Vimos que con Dagger Multi-vinculante, podemos proporcionar fácilmente dependencias a nivel de aplicación.


Un constructor que implemente todos nuestros componentes es extremadamente atractivo, e incluso podemos hacer algo para que funcione correctamente con instancias de contexto. Este es un futuro prometedor, pero solo está disponible a partir de la API 28. Si desea llegar a menos del 0.5% de los usuarios, puede probarlo. De lo contrario, debe esperar y ver si dicho método sigue siendo relevante en unos pocos años.

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


All Articles