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:
1 2 3 4 5 6 7 8 9 |
public class Aplicacion extends Application { @Override public void onCreate() { RoboGuice.setBaseApplicationInjector(this, RoboGuice.DEFAULT_STAGE, RoboGuice.newDefaultRoboModule(this), Modules.override(new LibraryModule()).with(new AppModule())); } } |
LibraryModule
y AppModule
) tenían el mismo provider pero devolvían instancias de clases distintas?
1 2 3 4 5 6 7 |
public class LibraryModule extends AbstractModule { @Override protected void configure() { bind(TagManager.class).to(TagManagerImpl.class).asEagerSingleton(); } } |
1 2 3 4 5 6 7 8 |
public class AppModule extends AbstractModule { @Override protected void configure() { bind(TagManager.class).to(AppTagManagerImpl.class).asEagerSingleton(); } } |
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:
1 2 3 4 5 6 7 8 9 10 |
@Module public class LibraryDaggerModule() { @Provides @Singleton TagManager provideTagManager() { return new TagManagerImpl(); } } |
1 2 3 4 5 6 7 8 9 |
@Module public class AppDaggerModule { @Provides @Singleton TagManager provideTagManager() { return new AppTagManagerImpl(); } } |
- 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 unTagManager
. 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:- Hacer que una librería pueda utilizar Dagger sin depender de la aplicación.
- Sobrescribir un módulo.
- No romper el patrón singleton al sobrescribir un modulo.
- Caso 1: la librería utiliza Dagger de forma interna sin que la aplicación tenga que saber nada de Dagger.
- Caso 2: igual que el anterior pero la aplicación usa Dagger aunque de forma independiente a la librería.
- 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.
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 llamadaDIManagerCajaHerramientas
, 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 unMartilloPercutor
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Module public class ModuloCajaHerramientas { @Provides String provideFormula() { return "Grasa hecha a base de componentes de calidad."; } @Provides @Singleton Grasa provideGrasa() { return new Grasa(); } } |
1 2 3 4 5 6 7 |
@Singleton @Component(modules = ModuloCajaHerramientas.class) public interface ComponenteCajaHerramientas { void inject(Grasa grasa); void inject(MartilloPercutor martilloPercutor); } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class DIManagerCajaHerramientas { private DIManagerCajaHerramientas() {} private static ComponenteCajaHerramientas sComponente; public static ComponenteCajaHerramientas getComponente() { if (sComponente == null) { sComponente = DaggerComponenteCajaHerramientas.builder().build(); } return sComponente; } } |
MartilloPercutor
sin tener que utilizar la clase aplicación.
1 2 3 4 5 6 7 8 |
public class MartilloPercutor { @Inject Grasa mGrasa; public MartilloPercutor() { DIManagerCajaHerramientas.getComponente().inject(this); } } |
.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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class MainActivity extends AppCompatActivity { @BindView(R.id.scroll) ScrollView scroll; @BindView(R.id.resultados) TextView resultados; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); incluirResultado(new Grasa().getDescripcion()); incluirResultado(new Grasa().getDescripcion()); incluirResultado(new Grasa().getDescripcion()); incluirResultado(new MartilloPercutor().getDescripcion()); incluirResultado(new MartilloPercutor().getDescripcion()); incluirResultado(new MartilloPercutor().getDescripcion()); } private void incluirResultado(String resultado) { String nuevoResultado = resultados.getText() + resultado + "\n\n"; resultados.setText(nuevoResultado); scrollHastaElFinal(); } private void scrollHastaElFinal() { scroll.post(new Runnable() { @Override public void run() { scroll.fullScroll(ScrollView.FOCUS_DOWN); } }); } } |
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:
1 2 3 4 5 6 7 8 9 |
@Module(includes = ModuloCajaHerramientas.class) public class ModuloTaller { @Provides @Singleton protected MartilloPercutor provideMartilloPercutor() { return new MartilloPercutor(); } } |
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:
1 2 3 4 5 |
@Singleton @Component(modules = ModuloTaller.class) public interface ComponenteTaller { void inject(MainActivity activity); } |
@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 unDIManager
para nuestra aplicación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DIManagerTaller { private static ComponenteTaller sComponente; private DIManagerTaller() {} public static ComponenteTaller getComponente() { if (sComponente == null) { sComponente = DaggerComponenteTaller.builder() .build(); } return sComponente; } } |
Uso
Haremos ahora un cambio en laMainActivity
para probar la inyección y ver qué pasa ahora:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public class MainActivity extends AppCompatActivity { @Inject Grasa mGrasa1; @Inject Grasa mGrasa2; @Inject Grasa mGrasa3; @Inject MartilloPercutor mMartilloPercutor1; @Inject MartilloPercutor mMartilloPercutor2; @Inject MartilloPercutor mMartilloPercutor3; @BindView(R.id.scroll) ScrollView scroll; @BindView(R.id.resultados) TextView resultados; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); DIManagerTaller.getComponente().inject(this); mostrarInformacion(); } public void mostrarInformacion() { incluirResultado(mGrasa1.getDescripcion()); incluirResultado(mGrasa2.getDescripcion()); incluirResultado(mGrasa3.getDescripcion()); incluirResultado(mMartilloPercutor1.getDescripcion()); incluirResultado(mMartilloPercutor2.getDescripcion()); incluirResultado(mMartilloPercutor3.getDescripcion()); } private void incluirResultado(String resultado) { String nuevoResultado = resultados.getText() + resultado + "\n\n"; resultados.setText(nuevoResultado); scrollHastaElFinal(); } private void scrollHastaElFinal() { scroll.post(new Runnable() { @Override public void run() { scroll.fullScroll(ScrollView.FOCUS_DOWN); } }); } } |
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
- En la aplicación creamos un módulo que herede del
ModuloCajaHerramientas
y se lo pasamos alDIManagerCajaHerramientas
en su métodoinit()
para inicializar elComponenteCajaHerramientas
. - El módulo sobrescrito mantiene el patrón singleton porque devolverá las instancias singleton que creemos en el
ModuloTaller
exponiéndolas en elComponenteTaller.
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 unaGrasaBarata
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:
1 2 3 4 5 6 |
public class GrasaBarata extends Grasa { public GrasaBarata() { usos = 5; } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ModuloSobrescrito extends ModuloCajaHerramientas { @Override protected String provideFormula() { return "Grasa hecha con componentes baratos."; } @Override protected Grasa provideGrasa() { return new GrasaBarata(); } } |
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
.
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Module public class ModuloTaller { @Provides @Singleton protected MartilloPercutor provideMartilloPercutor() { return new MartilloPercutor(); } @Singleton @Provides protected Grasa provideGrasa(String formula) { return new GrasaBarata(formula); } } |
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
:
1 2 3 4 5 6 7 8 |
@Singleton @Component(modules = { ModuloTaller.class }) public interface ComponenteTaller { void inject(MainActivity activity); Grasa getGrasa(); } |
ModuloSobrescrito
para que devuelva esta instancia cuando la librería las pida:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ModuloSobrescrito extends ModuloCajaHerramientas { @Override protected String provideFormula() { return "Grasa hecha con componentes baratos."; } @Override protected Grasa provideGrasa(String formula) { return DIManagerTaller.getComponente().getGrasa(); } } |
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 aComponenteCajaHerramientas
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class DIManagerCajaHerramientas { private DIManagerCajaHerramientas() {} private static ComponenteCajaHerramientas sComponente; public static void init(@Nullable ModuloCajaHerramientas modulo) { DaggerComponenteCajaHerramientas.Builder builder = DaggerComponenteCajaHerramientas.builder(); if (modulo != null) { builder.moduloCajaHerramientas(modulo); } sComponente = builder.build(); } public static ComponenteCajaHerramientas getComponente() { return sComponente; } } |
1 2 3 4 5 6 7 8 |
public class Aplicacion extends Application { @Override public void onCreate() { super.onCreate(); DIManagerCajaHerramientas.init(new ModuloSobrescrito()); } } |
Uso
¡Vamos a ejecutar de nuevo la misma MainActivity y comprobar qué pasa! ¿Qué estamos viendo aquí? Pues que hemos sobrescrito correctamente el provider deGrasa
. 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:- En
LibraryModule
declarar comoprotected
todos los métodos que queramos sobrescribir en la aplicación. - Crear en la aplicación un módulo que herede de
LibraryModule
:LibraryOverridenModule
. - En
AppModule
, declarar los métodos sobrescritos (supuestamente esto ya tiene que estar hecho) con su ámbito correcto. - Exponer todos estos métodos provider en el
AppComponent
. - De vuelta en el
LibraryOverridenModule
, sobrescribir todos los métodosprotected
llamando en cada uno al correspondiente provider expuesto en elAppComponent
. - Configurar el
DIManagerLibrary
para que tenga un métodoinit()
que reciba unLibraryModule
e inicialice con este elLibraryComponent
. - Llamarlo en nuestra clase
Application
pasándole una instancia deLibraryOverridenModule
.
Declarar providers como protected
En elLibraryModule
indicamos que el método provideTagManager()
es protegido para poder sobrescribirlo:
1 2 3 4 5 6 7 8 9 10 |
@Module public class LibraryDaggerModule() { @Provides @Singleton protected TagManager provideTagManager() { return new TagManagerImpl(); } } |
Crear el módulo sobrescrito
En la aplicación crea una clase que herede deLibraryModule
.
1 2 3 4 5 6 7 |
public class LibraryOverridenModule { @Override protected TagManager provideTagManager() { return super.provideTagManager(); } } |
@Module
porque dará error.
Módulo de la aplicación
Creamos el provider singleton deTagManager
que va a sobrescribir el de la librería en el módulo de la aplicación:
1 2 3 4 5 6 7 8 |
public class AppDaggerModule { @Provides @Singleton protected TagManager provideTagManager() { return new AppTagManagerImpl(); } } |
Exponer el provider
Exponemos el provider en elAppComponent
:
1 2 3 4 5 6 7 |
@Component public interface AppModule { [...] // Métodos inject declarados por aquí TagManager getTagManager(); } |
Usamos el provider expuesto en el módulo sobrescrito
Ahora modificamos el métodoproviderTagManager
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
:
1 2 3 4 5 6 7 |
public class LibraryOverridenModule { @Override protected TagManager provideTagManager() { return DIManagerApp.getAppComponent().getTagManager(); } } |
Crea los DIManager
Crea elDIManagerApp
para desacoplar el componente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DIManagerApp { private static AppComponent sComponent; private DIManagerApp() {} public static AppComponent getAppComponent() { if (sComponente == null) { sComponente = DaggerAppComponent.builder().build(); } return sComponente; } } |
DIManagerLibrary
con un método init()
que reciba un LibraryModule
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class DIManagerLibrary { private static LibraryComponent sComponent; private DIManagerLibrary{} public void init(@Nullable LibraryDaggerModule module) { if (sModule == null) { sComponent = DaggerLibraryComponent.builder().build(); } else { sComponent = DaggerLibraryComponent.builder.libraryModule(sModule).build(); } } public static LibraryComponent getLibComponent() { if (sComponente == null) { throw new IllegalStategException("¡Aún no has inicializado el LibraryComponent!); } else { return sComponent; } } } |
Inicializa el DIManagerLibrary
En la clase aplicación, inicializa elLibraryComponent
con una instancia de LibraryOverridenModule
:
1 2 3 4 5 6 7 |
public class Aplication extends Application { public void onCreate() { super.onCreate(); DIManagerLibrary.init(new LibraryOverridenModule()); } } |