Imagen de cabecera

De RoboGuice a Dagger 2 – Parte 2

Sobrecarga de módulos Esta es la segunda parte del tutorial de migración de RoboGuice a Dagger 2. Puedes leerla sin necesidad de haber leído la anterior, pero aquí tienes el enlace a las otras partes de todas formas:

Introducción

Como explicamos en la introducción del tutorial, la inicialización de RoboGuice en el proyecto consiste en esta orden: Esta orden sirve para crear el componente/grafo de objetos (por equipararlo a Dagger) para todo: para la aplicación y para la librería. ¿Recuerdas que en la primera parte de esta serie de tutoriales ambos módulos (LibraryModule y AppModule) tenían el mismo provider pero devolvían instancias de clases distintas? Pues bien, la octava línea de la clase aplicación indica que los providers de LibraryModule van a ser sobrescritos con los que hay en AppModule. De esta forma, cuando tanto la aplicación como la libería quieran inyectar un TagManager, inyectarán un AppTagManagerImpl. Al convertir los módulos a Dagger obtenemos esto: Dagger no tiene una opción tan sencilla para sobrescribir los módulos. O bien todos los módulos contribuyen a un mismo componente o bien hacemos algún apaño para que librería y aplicación puedan tener cada una su componente y que aun así la aplicación pueda sobrescribir los de la librería.
  • Si utilizamos Dagger y un solo componente para hacer lo que hace RoboGuice, habría que eliminar uno de los métodos provider anteriores, porque no puede haber providers repetidos en un componente. Parece lo mejor…, pero como hemos mencionado anteriormente, utilizar un solo componente significaría que este componente tendría que ser el de la aplicación, y hacer la librería tenga que conocer el componente de la aplicación haría que dejara de ser independiente porque ya no se podrían utilizar sin esta.
  • Por el contrario, si se usa un componente para la aplicación y otro para la librería tendríamos que encontrar una forma de sobrescribir el módulo del de la librería para que emulara el comportamiento de RoboGuice y devolviera un AppTagManagerImpl cuando sus clases solicitaran un TagManager. Y además, tendríamos que encontrar un mecanismo para hacer que la instancia fuera única porque está marcada como singleton.Pues eso es lo que vamos a hacer.

Recreación

Así pues tenemos tres problemas:
  1. Hacer que una librería pueda utilizar Dagger sin depender de la aplicación.
  2. Sobrescribir un módulo.
  3. No romper el patrón singleton al sobrescribir un modulo.
Vamos a ilustrar todo esto mejor mediante una aplicación más sencilla que la nuestra, pero puedes ir directamente a la solución para nuestra aplicación en el apartado Solución para nuestra aplicación. La aplicación que crearemos se llamará «Taller», y tendrá un módulo librería llamado «CajaHerramientas». Tendremos tres versiones de la aplicación para reproducir tres escenarios diferentes:
  1. Caso 1: la librería utiliza Dagger de forma interna sin que la aplicación tenga que saber nada de Dagger.
  2. Caso 2: igual que el anterior pero la aplicación usa Dagger aunque de forma independiente a la librería.
  3. Caso 3: igual que la anterior pero esta vez la aplicación quiere sobrescribir los providers de la librería. Es decir, la aplicación «configura» las dependencias de la librería.
El caso 3 será en el que se solucionen todos los problemas, pero es conveniente ir viendo los dos anteriores para entender por qué lo hacemos así.

Caso 1: Aplicación sin Dagger, librería con Dagger

Resumen

La librería crea su componente y se inyectará sus clases ella sola mediante una clase llamada DIManagerCajaHerramientas, que es un singleton encubierto y desacoplado de cualquier clase Application.

Planteamiento

Aquí tienes el proyecto en GitHub. En nuestra caja de herramientas vamos a tener un MartilloPercutor y un bote de Grasa para el martillo. Cada vez que se utilice el martillo percutor será necesario engrasarlo. Cuando no haya más grasa, el martillo percutor no podrá utilizarse. Aquí puedes ver las clases MartilloPercutor y Grasa. Pero lo importante viene en la clase ModuloCajaHerramientas: Siempre vamos a querer la misma grasa para engrasar los martillos percutores. A continuación aquí puedes ver el ComponenteCajaHerramientas:

Problema y solución

Bien, ahora veamos. Debe existir un lugar donde instanciemos el componente que nos garantice que no se va a destruir durante toda la vida de la aplicación. ¿Dónde está eso? Pues en la clase aplicación. Pero esto es una biblioteca, ¿dónde está la clase aplicación? … … … ¡¡¡¡¡OMFG, no hay!!!!! ¿La solución? Crearemos una clase que sirva para desacoplar el componente de la aplicación. DIManagerCajaHerramientas.class Esta clase (DIManager -> Dependency Injection Manager) sirve para que la instancia del componente esté fuera de la clase aplicación. Gracias a esto podemos inicializar la inyección en la clase MartilloPercutor sin tener que utilizar la clase aplicación. Nota: no llamamos al método .moduloCajaHerramientas() porque no es necesario. Cuando el módulo se construye mediante su constructor vacío, Dagger lo inicializa de esta manera por defecto.

Uso

Vamos a utilizar clases de la librería para comprobar el funcionamiento de nuestro componente. Hemos creado un layout simplemente con un texto scrollable y esta es la activity que lo usa: Ejecutar esta aplicación produce el siguiente resultado:
Resultado de taller 1

Ejecución de Taller 1

¿Qué significa esto? Cada nueva instancia de MartilloPercutor recibe la misma Grasa (provista por la implementación interna de la librería) porque, aunque estemos instanciándolo en la aplicación, el constructor que está en la librería le inyecta la instancia singleton de Grasa. Sabemos que la instancia es la misma porque todas tienen el mismo número, que en realidad es su hashcode. De esta forma, hemos conseguido que la librería CajaHerramientas utilice Dagger y la aplicación no tenga que saber nada sobre ello.

Caso 2: includes y modules

Ahora queremos que tanto la aplicación como la librería utilicen Dagger y que la aplicación pueda beneficiarse de los providers del módulo de la librería.

Resumen

El módulo de la aplicación incluirá el módulo de la librería para poder utilizar los mismos providers. El patrón singletón se romperá, ya que habrá dos componentes y cada uno produce una instancia singleton que es distinta de la del otro, aunque vengan del mismo módulo.

Planteamiento

Aquí tienes el proyecto en GitHub. A continuación queremos inyectar grasa e inyectar martillos percutores en nuestra aplicación en lugar de tener que crear instancias manualmente. Para ello, vamos a implementar Dagger 2 en la aplicación utilizando el siguiente componente: Con includes hacemos que ModuloTaller ahora contenga también todos los providers de String y de Grasa. Por eso solamente declaramos un provider de MartilloPercutor, porque es el único que falta en el ModuloCajaHerramientas y que queremos usar en nuestra aplicación. Este es el componente: Tanto esto: @Module(includes = ModuloCajaHerramientas.class) como esto: @Component(modules = { ModuloTaller.class, ModuloCajaHerramientas.class }) es lo mismo. Al final ComponenteTaller se contruirá con la suma de los providers de ambos módulos.

DIManagerTaller

También utilizaremos un DIManager para nuestra aplicación:

Uso

Haremos ahora un cambio en la MainActivity para probar la inyección y ver qué pasa ahora: Y este es el resultado:
Resultado Taller 2

Ejecución de Taller 2

¿Qué significa esto? Como puedes ver, las tres primeras instancias de Grasa, inyectadas por el ComponenteTaller, son una instancia única. Sin embargo, como era de esperar, es una instancia única diferente de la de la grasa inyectada en los martillos percutores, que está inyectada por ComponenteCajaHerramientas. La conclusión era lo que ya habíamos vaticinado: aunque los dos componentes usen el mismo módulo, la instancia es única solamente si la genera el mismo componente. Vamos a ver cómo solucionamos esto.

Caso 3: sobrescritura de módulo

Queremos que tanto la aplicación como la librería utilicen Dagger 2, que las instancias únicas sigan siendo únicas a pesar de ser provistas por diferentes componentes, y que además la aplicación pueda sobrescribir los métodos provider del módulo.

Resumen

  1. En la aplicación creamos un módulo que herede del ModuloCajaHerramientas y se lo pasamos al DIManagerCajaHerramientas en su método init()para inicializar el ComponenteCajaHerramientas.
  2. El módulo sobrescrito mantiene el patrón singleton porque devolverá las instancias singleton que creemos en el ModuloTaller exponiéndolas en el ComponenteTaller.

Planteamiento

Aquí tienes el proyecto en GitHub. No estamos contentos con nuestra caja de herramientas porque la grasa que hay en esta para los martillos percutores es carilla y hay que reducir costes porque estamos en crisis. Lo que necesitamos es que el taller sea el que decida qué grasa es la que se va a inyectar en los martillos. Es decir, queremos decidir qué Grasa va a devolver el ModuloCajaHerramientas. Para eso no nos queda más remedio que sobrescribirlo.

Sobrescribir el módulo

Queremos que el módulo devuelva una GrasaBarata y una fórmula más barata. Esta es la clase GrasaBarata, declarada en la aplicación y que desgraciadamente solo tiene 5 usos, pero es lo que hay para gente como nosotros, que tenemos un taller pobre: Creamos un nuevo módulo en la aplicación que herede de ModuloCajaHerramientas y lo llamamos ModuloSobrescrito. Esta clase no debe tener la anotación @Module, ya que ya la lleva la clase padre. En ella sobrescribiremos el método provideGrasa() para que devuelva una GrasaBarata en lugar de la Grasa normal. Sobra decir que los métodos que quieras sobrescribir tendrán que ser públicos o protegidos:
Inconvenientes
Por desgracia, esta sobrescritura tiene las siguientes limitaciones:
  • No puedes cambiar la signatura de los métodos sobrescritos. En caso de que quisieras hacerlo, tendrás que utilizar la anotación @Named necesariamente, de forma que no haya problemas de duplicidad de providers.
  • No puedes especificar un ámbito. En una clase sin la anotación @Module no se puede poner la anotación @Singleton (puedes, pero no tiene efecto), por lo que todos los métodos perderán la capacidad de generar instancias únicas. En el siguente punto vemos cómo solucionarlo.
  • No pueden declarar providers nuevos. En una case sin la anotación @Module no se puede usar la anotación @Provides. Usarla provocará un error al compilar. Solo se pueden sobrescribir los métodos existentes; los nuevos serán ignorados. Pero esto nos da igual porque para declarar nuevos providers utiliza el módulo de la aplicación y listo.
  • No se puede sobrescribir el valor del atributo @Named; se queda con el valor con el que lo declara el módulo padre. Si quieres especificar un método con un @Named distinto del original, igual que en el punto anterior, hazlo en el módulo de la aplicación. Puedes hacerlo sin peligro ninguno de duplicidad, ya que ese es precisamente el uso de la anotación @Named.
Puedes leer más sobre este asunto en https://google.github.io/dagger/testing.html, en el apartado Option 1: Override bindings by subclassing modules (don’t do this!).

Cambiar el ámbito de las nuevas inyecciones

Dado que al sobrescribir el módulo no podemos marcar los métodos provider con @Singleton, perdemos la capacidad de generar instancias únicas. Lo que haremos para que el método sobrescrito provideGrasa() pueda seguir siendo singleton será utilizar el módulo de la aplicación para declarar un provider que sí tengan ámbito y exponerlo en el componente de la aplicación para poder llamarlo en el módulo sobrescrito. Es decir, ModuloSobrescrito tomará sus dependencias de los providers expuestos en ComponenteTaller, declarados en ModuloTaller. Vamos al módulo ModuloTaller: Como observarás, en el módulo tenemos el provider de MartilloPercutor que declaramos antes y ahora hemos añadido un provider que devuelve GrasaBarata. No declaramos el provider de la fórmula ya que solo la queremos sobrescribir, pero no necesitamos que esta sea singleton. ¿Ahora qué tenemos? El ModuloTaller devuelve una instancia única de Grasa, que en realidad es una GrasaBarata cuando alguien la solicite desde su componente. Es decir, que inyectará GrasaBarata en la aplicación. ¿Cómo hacemos para que ModuloSobrescrito devuelva esta instancia única? Vamos a exponer este provider en ComponenteTaller: Este provider expuesto conserva el ámbito con el que ha sido declarado en el módulo. Y ahora modificamos ModuloSobrescrito para que devuelva esta instancia cuando la librería las pida: Y con esto ya nos estamos asegurando de que tanto ModuloTaller como ModuloSobrescrito devuelvan la misma instancia de Grasa. Fíjate además en que ahora ni ModuloTaller ni ComponenteTaller incluyen a ModuloCajaHerramientas, ya que, de hacerlo, el provider de Grasa estaría duplicado, al estar declarado en ambos módulos. Si a pesar de eso necesitas incluir el ModuloCajaHerramientas en el ModuloTaller porque tengas más providers que quieres heredar, puedes dividir ModuloCajaHerramentas en dos: uno que contenga todo lo que va a ser sobrescrito y otro con aquello que no, para que podamos incluir este segundo módulo sin conflictos de duplicidad.

Modificar el DIManagerCajaHerramientas

Ahora ¿cómo le pasamos a ComponenteCajaHerramientas una instancia de ModuloSobrescrito en lugar de su propio ModuloCajaHerramientas? Muy sencillo: añadiremos a la clase DIManagerCajaHerramientas un método inicializador que reciba el módulo que queremos utilizar: Y ya solo nos queda inicializarlo en la clase aplicación de nuestro proyecto:

Uso

¡Vamos a ejecutar de nuevo la misma MainActivity y comprobar qué pasa!
Ejecución de la MainActivity, donde se aprecia que tanto la grasa inyectada en la aplicación como la inyectada en los martillos percutores es la misma instancia única.

Ejecución de Taller 3

¿Qué estamos viendo aquí? Pues que hemos sobrescrito correctamente el provider de Grasa. Ahora la instancia única de Grasa inyectada en la aplicación es la misma que la que reciben los martillos percutores porque, aunque esta segunda sea inyectada por ComponenteCajaHerramientas, este está devolviendo la misma instancia que el ComponenteTaller. ¡Objetivo cumplido!

Solución para nuestra aplicación

Entonces, de vuelta a la aplicación con la que empezamos en la parte 1, ¿qué tendríamos que hacer? A modo de resumen:
  1. En LibraryModule declarar como protected todos los métodos que queramos sobrescribir en la aplicación.
  2. Crear en la aplicación un módulo que herede de LibraryModule: LibraryOverridenModule.
  3. En AppModule, declarar los métodos sobrescritos (supuestamente esto ya tiene que estar hecho) con su ámbito correcto.
  4. Exponer todos estos métodos provider en el AppComponent.
  5. De vuelta en el LibraryOverridenModule, sobrescribir todos los métodos protected llamando en cada uno al correspondiente provider expuesto en el AppComponent.
  6. Configurar el DIManagerLibrary para que tenga un método init() que reciba un LibraryModule e inicialice con este el LibraryComponent.
  7. Llamarlo en nuestra clase Application pasándole una instancia de LibraryOverridenModule.

Declarar providers como protected

En el LibraryModule indicamos que el método provideTagManager() es protegido para poder sobrescribirlo:

Crear el módulo sobrescrito

En la aplicación crea una clase que herede de LibraryModule. Recuerda no marcar esta clase como @Module porque dará error.

Módulo de la aplicación

Creamos el provider singleton de TagManager que va a sobrescribir el de la librería en el módulo de la aplicación:

Exponer el provider

Exponemos el provider en el AppComponent:

Usamos el provider expuesto en el módulo sobrescrito

Ahora modificamos el método providerTagManager del módulo LibraryOverridenModule para que devuelva la instancia singleton del módulo de la aplicación provista por el provider expuesto en el AppComponent:

Crea los DIManager

Crea el DIManagerApp para desacoplar el componente: Y el DIManagerLibrary con un método init() que reciba un LibraryModule:

Inicializa el DIManagerLibrary

En la clase aplicación, inicializa el LibraryComponent con una instancia de LibraryOverridenModule: Y hasta aquí la sobrecarga de módulos de Dagger sin romper el patrón singleton. La última parte de este tutorial consiste en eliminar las posibles dependencias cíclicas que se hayan podido formar o que ya existían antes de la migración.
0 Comentarios

Contesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

©2023 Codictados Comunidad libre para el aprendizaje de codigo Online

o

Inicia Sesión con tu Usuario y Contraseña

o    

¿Olvidó sus datos?

o

Create Account