De RoboGuice a Dagger 2 – Parte 1
Conversión de RoboGuice a Dagger Esta es la primera parte del tutorial de migración de RoboGuice a Dagger 2. Aquí tienes el enlace a las otras dos partes:Introducción
Nuestra misión en esta primera parte será eliminar RoboGuice, convirtiendo los módulos de RoboGuice en módulos de Dagger. Por suerte, RoboGuice utiliza módulos para hacer métodos provider y utiliza las mismas anotaciones (de paquetes distintos) aunque en lugares diferentes, para hacer singletons. Vamos a explicar todo lo que probablemente vayamos a encontrar en un módulo de RoboGuice y cómo convertirlo a la sintaxis de Dagger. Este es un esquema de todos los pasos que tendremos que seguir para eliminar RoboGuice de nuestra aplicación y sustituirlo por Dagger:- Convertir el módulo de RoboGuice a un módulo de Dagger
- Revisar todas las clases
- Crear el DIManager
- Eliminar el resto de basurilla
- Arrancar
Tabla de conversiones
Este es un cuadro resumen de las conversiones que vamos a realizar:RoboGuice | Explicación | Dagger 2 |
---|---|---|
bind() |
Define un provider para una clase que crea una instancia utilizando el constructor vacío de esa clase o uno con objetos inyectables | @Provides |
bind().to() |
Define un provider para una interfaz | @Provides |
bind().toInstance() |
Define un provider para una clase que necesita ser configurada | @Provides |
.asEagerSingleton() |
Indica que la instancia generada sea única y que se cree en cuanto se inicialice RoboGuice | @Singleton |
TypeLiteral<> |
Devuelve una instancia de un objeto que utiliza un tipo genérico | @Provides |
.annotatedWith(named()) |
Diferencia a este provider de otros que devuelvan una instancia del mismo tipo que el suyo | @Named |
com.google.inject.@Provides |
Igual que en Dagger | javax.inject.@Provides |
@Named() |
Igual que en Dagger | @Named() |
com.google.inject.@Singleton |
Se pone marcando una clase para indicar que las instancias que se generen para inyectar deben ser siempre la misma | javax.inject.@Singleton |
com.google.inject.@Inject |
Se pone marcando los campos de una clase para indicar que son campos inyectados | javax.inject.@Inject |
com.google.inject.Provider |
Objeto utilizado para generar instancias de una clase | javax.inject.Provider |
RoboActivity |
Activity que inicializa la inyección de dependencias por defecto | AppCompatActivity |
RoboFragmentActivity |
FragmentActivity que inicializa la inyección de dependencias por defecto | AppCompatActivity |
RoboFragment |
Fragment que inicializa la inyección de dependencias por defecto | Fragment |
RoboDialogFragment |
DialogFragment que inicializa la inyección de dependencias por defecto | Fragment |
RoboService |
Service que inicializa la inyección de dependencias por defecto | Service |
RoboBroadcastReceiver |
BroadcastReceiver que inicializa la inyección de dependencias por defecto | BroadcastReceiver |
RoboSimpleAsyncTaskLoader |
AsyncTaskLoader que inicializa la inyección de dependencias por defecto | AsyncTaskLoader |
1. Convertir el módulo de RoboGuice a un módulo de Dagger
En RoboGuice un módulo es una clase que hereda deAbstractModule
. Nuestro proyecto es una aplicación con una librería. Ambos usan RoboGuice. Esta es (una versión extremadamente simplificada de) la clase módulo de la librería:
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 46 47 48 49 50 51 52 |
public class LibraryModule extends AbstractModule { private final Context context; public LibraryModule(Context context) { this.context = context; } @Override protected void configure() { bind(MobileSorter.class); bind(MobileManager.class); bind(Executor.class); bind(PatchClientManager.class).to(PatchClientManagerImpl.class); bind(SynchroManager.class).to(SynchroManagerImpl.class); bind(SyncServiceManager.class).to(SyncServiceManagerImpl.class); bind(Picasso.class).toInstance(Picasso.with(context)); bind(TagManager.class).to(TagManagerImpl.class).asEagerSingleton(); bind(new TypeLiteral<Parser<Map<String, Integer>>>() {}) .annotatedWith(named(MasterParser.NAME)) .to(new TypeLiteral() {}); bind(new TypeLiteral<Parser<Map<String, Integer>>>() {}) .annotatedWith(named(SecondaryParser.NAME)) .to(new TypeLiteral() {}); } @Provides public XmlProcessor provideXmlProcessor(final XmlConfiguration xmlConfiguration) { return new XmlProcessor() { @Override public XmlConfiguration getConfig() { return new XmlConfiguration.getDefault(); } } } @Provides @Named(MainActivity.TAG) public Collection provideObjectList() { List objectList = new ArrayList<>(); objectList.add(new Object1()); objectList.add(new Object2()); objectList.add(new Object3()); return objectList; } } |
1 2 3 4 5 6 7 8 9 |
public class ApplicationModule extends AbstractModule { @Override protected void configure() { bind(Executor.class).to(AppExecutor.class); bind(TagManager.class).to(AppTagManagerImpl.class).asEagerSingleton(); } } |
1 2 3 4 5 6 7 8 9 10 |
@Module public class LibraryDaggerModule { private Context context; public LibraryDaggerModule(Context context) { this.context = context; } } |
1 2 3 4 |
@Module public class ApplicationDaggerModule { } |
bind(), bind().to() y bind().toInstance()
bind()
indica que la instancia se construye con el constructor vacío de la clase o con uno que reciba parámetros que también sean inyectables. Por ejemplo bind(MobileSorter.class)
indica que la instancia se construirá con el constructor vacío de la clase MobileSorter
. Por lo tanto, sustituimos este código por un provider en el módulo de Dagger:
1 2 3 4 5 6 7 8 |
@Module public class LibraryDaggerModule() { @Provides MobileSorter provideMobileSorter() { return new MobileSorter(); } } |
bind(MobileManager.class)
comprobamos la clase MobileManager
y observamos que su constructor recibe una instancia de MobileSorter
:
1 2 3 4 5 6 7 8 |
public class MobileManager { private MobileSorter sorter; public MobileManager(MobileSorter sorter) { this.sorter = sorter; } } |
bind(MobileManager.class)
tiene que haber un provider de MobileSorter
, es decir, un bind(MobileSorter)
. Por lo que al convertirlo a Dagger, hemos de asegurarnos de que hay dos providers y que el de MobileManager
recibe como parámetro un MobileSorter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Module public class LibraryDaggerModule() { @Provides MobileSorter provideMobileSorter() { return new MobileSorter(); } @Provides MobileSorter provideMobileManager(MobileSorter sorter) { return new MobileSorter(sorter); } } |
bind().to()
es lo mismo que el anterior pero sirve para proporcionar instancias de interfaces. Así bind(PatchClientManager.class).to(PatchClientManagerImpl.class)
indica que una instancia de PatchClientManager
se creará mediante el constructor vacío de PatchClientManagerImpl
o con un constructor que reciba parámetros inyectables. Por lo tanto lo sustituimos por un provider en el módulo de Dagger:
1 2 3 4 5 6 7 8 |
@Module public class LibraryDaggerModule() { @Provides PatchClientManager providePatchClientManager() { return new PatchClientManagerImpl(); } } |
bind().toInstance()
indica que la instancia que hay que devolver al pedir un objeto de esa clase debe configurarse, es decir, que no nos vale con el constructor vacío, sino que hay que generar la instancia de otra manera. Así, bind(Picasso.class).toInstance(Picasso.with(context));
indica que para obtener una instancia de Picasso
hay que llamar a Picasso.with(context)
, lo cual devolverá la instancia necesaria. Por lo tanto, lo sustituimos por un provider en el módulo de Dagger:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Module public class LibraryDaggerModule() { private Context context; public LibraryDaggerModule(Context context) { this.context = context; } @Provides Picasso providePicasso() { return Picasso.with(context); } } |
.asEagerSingleton()
Este método hace que la instancia sobre la que se aplica sea única y que se cree en cuanto se inicialice RoboGuice (verdaderamente, que se inicialice con RoboGuice o que se inicialice la primera vez que la inyectemos nos es indiferente). Asíbind(TagManager.class).to(TagManagerImpl.class).asEagerSingleton()
devolverá una instancia única de TagManagerImpl
creada mediante su constructor vacío. Por lo tanto, lo sustituimos por un provider con @Singleton
en el módulo de Dagger:
1 2 3 4 5 6 7 8 9 10 11 |
import javax.inject.Singleton; @Module public class LibraryDaggerModule() { @Provides @Singleton TagManagerImpl provideTagManager() { return new TagManagerImpl(); } } |
TypeLiteral y annotatedWith(named())
TypeLiteral
es un tipo de objeto que RoboGuice utiliza para poder devolver objetos tipados. Por ejemplo, no puede hacer lo siguiente:
1 2 3 4 5 6 7 8 |
bind(List<String>).toInstance(getList()); private List getList() { List lista = new List<>(); lista.add("Una palabra"); lista.add("Dos palabras"); return lista; } |
1 |
bind(new TypeLiteral<List>() {}).to(new TypeLiteral() {}); |
1 2 |
bind(new TypeLiteral<Parser<Map<String, Integer>>>() {}) .to(new TypeLiteral() {}); |
1 2 3 4 5 6 7 8 |
@Module public class LibraryDaggerModule() { @Provides Parser<Map<String, Integer>> provideParser() { return new MasterParser(); } } |
MasterParser
implementa Parser<Map<String, Integer>>
:
1 2 |
public class MasterParser implements Parser<Map<String, Integer>> { } |
annotatedWith(named())
es autoexplicativo. Anota el método con @Named
para diferenciarlo de otros con el mismo tipo de retorno. Así este código con dos providers para un Parser<Map<String, Integer>>
:
1 2 3 4 5 6 7 |
bind(new TypeLiteral<Parser<Map<String, Integer>>>() {}) .annotatedWith(named(MasterParser.NAME)) .to(new TypeLiteral() {}); bind(new TypeLiteral<Parser<Map<String, Integer>>>() {}) .annotatedWith(named(SecondaryParser.NAME)) .to(new TypeLiteral() {}); |
@Named
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Module public class LibraryDaggerModule() { @Provides @Named(MasterParser.NAME) Parser<Map<String, Integer>> provideParser() { return new MasterParser(); } @Provides @Named(SecondaryParser.NAME) Parser<Map<String, Integer>> provideParser() { return new SecondaryParser(); } } |
@Provides y @Named()
Parece ser que se puede usar@Provides
como forma anotada de hacer un bind().toInstance()
y @Named
como forma anotada de annotatedWith(named())
. Esto lo sustituimos por las mismas anotaciones pero del paquete javax.inject en el caso de @Provides
. Así, este código se quedaría igual, pero asegúrate de que el paquete de las anotaciones es el correcto:
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 |
@Module public class LibraryDaggerModule() { @Provides @Named(MasterParser.NAME) Parser<Map<String, Integer>> provideParser() { return new MasterParser(); } @Provides @Named(SecondaryParser.NAME) Parser<Map<String, Integer>> provideParser() { return new SecondaryParser(); } @Provides public XmlProcessor provideXmlProcessor(final XmlConfiguration xmlConfiguration) { return new XmlProcessor() { @Override public XmlConfiguration getConfig() { return new XmlConfiguration.getDefault(); } } } @Provides @Named(MainActivity.TAG) public Collection provideObjectList() { List objectList = new ArrayList<>(); objectList.add(new Object1()); objectList.add(new Object2()); objectList.add(new Object3()); return objectList; } } |
Resultado
Los módulos de Dagger resultantes serían (inicialmente) así:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
@Module public class LibraryDaggerModule() { private Context context; public LibraryDaggerModule(Context context) { this.context = context; } @Provides MobileSorter provideMobileSorter() { return new MobileSorter(); } @Provides Executor provideExecutor() { return new Executor(); } @Provides PatchClientManager providePatchClientManager() { return new PatchClientManagerImpl(); } @Provides SynchroManager provideSynchroManager() { return new SynchroManagerImpl(); } @Provides SyncServiceManager provideSyncServiceManager() { return new SyncServiceManagerImpl(); } @Provides Picasso providePicasso() { return Picasso.with(context); } @Provides @Singleton TagManager provideTagManager() { return new TagManagerImpl(); } @Provides public XmlProcessor provideXmlProcessor(final XmlConfiguration xmlConfiguration) { return new XmlProcessor() { @Override public XmlConfiguration getConfig() { return new XmlConfiguration.getDefault(); } } } @Provides @Named(MainActivity.TAG) public Collection provideObjectList() { List objectList = new ArrayList<>(); objectList.add(new Object1()); objectList.add(new Object2()); objectList.add(new Object3()); return objectList; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Module public class AppDaggerModule { @Provides Executor provideExecutor() { return new AppExecutor(); } @Provides @Singleton TagManager provideTagManager() { return new AppTagManagerImpl(); } } |
2. Revisar todas las clases
Ahora viene una parte más larga y tediosa que la anterior. Tenemos que ir abriendo una por una todas las clases de todos los paquetes de nuestra aplicación e ir realizando los siguientes cambios.2.1. Mover la anotación singleton
En RoboGuice la anotación@Singleton
es del paquete com.google.inject y se antepone al nombre de la clase en su declaración. Si encontramos alguna, debemos moverla al módulo. Así este código:
1 2 3 4 5 6 |
import com.google.inject.Singleton; @Singleton public class ApplicationLifecycleManager { [...] } |
1 2 3 4 5 6 7 8 9 10 11 |
import javax.inject.Singleton; @Module public class LibraryDaggerModule() { @Provides @Singleton ApplicationLifeCycleManager provideApplicationLifeCycleManager() { [...] } } |
@Singleton
que usas es la del paquete javax.inject
.
2.2. Cambiar la anotación @Inject
Por fortuna, esta anotación marca los campos inyectados igual que en Dagger. Lo único que tenemos que hacer en cada clase que la encontremos es:- Eliminar la importación
com.google.inject.@Inject
y cambiarla por la dejava.inject.@Inject
. - Añadir al componente un método
inject()
para inicializar la inyección en esa clase y llamarlo donde corresponda.
1 2 3 4 5 6 7 8 9 10 11 12 |
import com.google.inject.Inject public class MainActivity { @Inject UserData mUserData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUserData().hacerAlgo(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import javax.inject.Inject public class MainActivity { @Inject UserData mUserData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); DIManager.getAppComponent().inject(this); mUserData.hacerAlgo(); } } |
RoboGuice.getInjector()
. Este inyector lo sustituimos por nuestra llamada al componente. Así este código:
1 2 3 4 5 6 7 8 9 10 11 |
import com.google.inject.Inject public class DiamondManager { @Inject PriceData priceData; public DiamondManager() { RoboGuice.getInjector(getContext()).injectMembers(this); priceData.hacerAlgo(); } } |
1 2 3 4 5 6 7 8 9 10 11 |
import javax.inject.Inject public class DiamondManager { @Inject PriceData priceData; public DiamondManager() DIManager.getAppComponent().inject(this); priceData.hacerAlgo(); } } |
2.3. De Provider a Provider
En RoboGuice hay un objetoProvider
. Su función es la de crear una instancia nueva cada vez que se solicite. Se utiliza, al igual que en Dagger, cuando en una misma clase vas a necesitar una cantidad indeterminada de inyecciones del mismo tipo. Lo único que tenemos que hacer en cada clase en la que la encontremos es sustituir la importación de com.google.inject.Provider
por la de javax.inject.Provider
. Y cuidado, tampoco la confundas con la anotación @Provides
.
Y si no nos gusta esta opción, podemos sustituirlo por una inyección puntual exponiendo el provider en el componente. Así este código:
1 2 3 4 5 6 7 8 9 |
public class CartManager { @Inject Provider executorProvider; [...] executorProvider.get().hacerAlgo(); } |
1 2 3 4 5 6 7 8 9 |
public class LibraryModule extends AbstractModule { @Override protected void configure() { bind(Executor.class); } } |
1 2 3 4 5 6 7 8 |
@Module public class LibraryDaggerModule() { @Provides Executor provideExecutor() { return new Executor(); } } |
1 2 3 4 5 |
@Component(modules = LibraryDaggerModule.class) public class LibraryComponent() { Executor getExecutor(); } |
1 2 3 4 |
public class CartManager { getComponent().getExecutor().hacerAlgo(); } |
2.4. De RoboClases a Clases
Encontraremos que RoboGuice obliga al uso deRoboActivity
, RoboFragment
y un montón de clases más que comienzan por «Robo». Pues todo lo que sea un Robo lo cambiamos por la clase normal:
- RoboActivity -> AppCompatActivity
- RoboFragment -> Fragment (support-v4)
- RoboFragmentActivity -> AppCompatActivity
- RoboDialogFragment -> DialogFragment
- RoboService -> Service
- RoboBroadcastReceiver -> BroadcastReceiver
- RoboSimpleAsyncTaskLoader -> AsyncTaskLoader
BaseFragment
, BaseActivity
…).
Además, para los RoboBroadcastReceiver
, hay que cambiar el método handleReceive()
por onReceive()
. Así esta clase:
1 2 3 4 5 6 7 8 9 10 11 |
public class DataReceiver extends RoboBroadcastReceiver { @Inject SyncServiceManager mSyncServiceManager; @Override public void handleReceive(Context context, Intent intent) { if ("android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { mSyncServiceManager.scheduleSyncService(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
public class DataReceiver extends BroadcastReceiver { @Inject SyncServiceManager mSyncServiceManager; @Override public void onReceive(Context context, Intent intent) { if ("android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { DIManager.getAppComponent().inject(this); mSyncServiceManager.scheduleSyncService(); } } } |
3. Crear el DIManager
Si te fijas, para acceder al componente hemos estado utilizando una clase llamadaDIManager
. Como hay una librería y una aplicación, cada una tiene su propio componente (el por qué se explica en la parte 2) y cada componente tendrá un DIManager
, que sirva para inicializarlo y acceder a él:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DIManagerApp { private static AppComponent sComponente; private DIManagerApp() {} public static AppComponent getComponent() { if (sComponente == null) { sComponente = DaggerAppComponent.builder() .build(); } return sComponente; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class DIManagerLibrary { private static LibraryComponent sComponente; private static Context sContext; private DIManagerLibrary() {} public static init(Context context) { sContext = context; } public static LibraryComponent getComponent() { if (sComponente == null) { sComponente = DaggerLibraryComponent.builder() .libraryDaggerModule(new LibraryDaggerModule(sContext)) .build(); } return sComponente; } } |
DIManagerLibrary
llamando al método init()
para poder pasarle un contexto. Esto lo harás en la clase aplicación:
1 2 3 4 5 6 7 |
public class Aplicacion extends Application { public void onCreate() { super.onCreate(); DIManagerLibrary.init(this); } } |
4. Eliminar el resto de basurilla
Y ahora eliminaremos las dependencias de RoboGuice de la aplicación. Ve al build.gradle tanto de la aplicación como de la librería y elimina todo lo que haya de RoboGuice. También ve al AndroidManifest.xml de la aplicación y busca elementos<meta%gt;
dentro del elemento application
que tengan esta apariencia:
1 2 3 4 |
<application ...> <meta-data android:name="roboguice.modules" android:value="com.example.AppModule,com.example.LibraryModule" /> </application> |
5. Arrancar – Ensayo y error
Y ahora llega el momento cumbre. «¡Trata de arrancarlo, Carlos!» Esta es la fase de ensayo y error. Intentarás compilar y comenzarás a recibir fallos. Los más comunes que verás:- Nos hemos dejado alguna anotación del paquete com.google.inject por ahí perdida
- Nos faltan dependencias para construir el grafo o tenemos dependencias duplicadas
- Se detectan dependencias cíclicas (se solucionarán en la parte 3 de esta serie de artículos)
AppDaggerModule
como elLibraryDaggerModule
tienen un provider de TagManager
y, si recuerdas la inicialización de RoboGuice, tenemos que hacer que la instancia sea la misma tanto cuando la pide la librería internamente como cuando la pide la aplicación (Modules.override(new LibraryModule()).with(new AppModule()))
); Avanza a la segunda parte de esta serie de artículos.