La inyección de dependencias (Dependency Injection, DI) es un patrón de diseño que ayuda a hacer nuestras aplicaciones más modulares y fáciles de testear, mantener y evolucionar. Su uso está más que asumido en el mundo Java gracias a Spring y al estándar JEE, y en Android se está utilizando cada vez más de la mano de Dagger 2, herramienta desarrollada por Google utilizando como base Dagger de Square y que también puede utilizarse en Java.
Puesto que existe mucha documentación en español sobre DI, este tutorial será eminentemente práctico y veremos cómo podemos empezar a utilizar Dagger 2 en nuestras aplicaciones para Android.
Proyecto de ejemplo
Partimos de un proyecto de ejemplo consistente en la siguiente Activity que muestra dos TextView en pantalla. Para uno de ellos tenemos que hacer uso de una AsyncTask ya que lo rellenamos con datos obtenidos desde un servicio rest que veremos a continuación.
package com.danielme.android.dagger2; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.widget.TextView; import java.io.IOException; public class MainActivity extends AppCompatActivity { private final SingletonService singletonService = SingletonServiceImpl.getInstance(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ((TextView) findViewById(R.id.textView1)).setText(singletonService.getPath(this)); (new ArtistTask()).execute(); } private class ArtistTask extends AsyncTask<Void, Void, Artist> { @Override protected Artist doInBackground(Void... params) { try { return singletonService.getArtist("bbd80354-597e-4d53-94e4-92b3a7cb8f2c"); } catch (IOException e) { return null; } } @Override protected void onPostExecute(Artist result) { TextView textView = (TextView) findViewById(R.id.textView2); if (result == null) { textView.setText(getString(R.string.error)); } else { textView.setText(result.getName()); } } } }
SingletonServiceImpl es una clase de servicio que contiene lógica de negocio definida en una interfaz. Tiene un método que necesita recibir el contexto de la aplicación, elemento de Android que habitualmente tenemos que ir pasando desde las activities hasta nuestras de clases de negocio o bien guardar en algún atributo estático.
El otro método de esta interfaz busca los datos de un artista utilizando la API de MusicBrainz. La obtención de datos se realiza haciendo uso del framework Retrofit que recomiendo encarecidamente para consumir servicios rest. No lo veremos en este tutorial pero en resumen con Retrofit lo que hacemos es modelar la API REST en interfaces utilizando anotaciones y el framework, totalmente configurable, se encarga del resto por nosotros, incluyendo el mapeo automático del JSON recibido en una clase Java. Esta es la interfaz que vamos a utilizar.
package com.danielme.android.dagger2; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; public interface ArtistRepository { @GET("artist/{mbid}?fmt=json") Call<Artist> getArtist(@Path("mbid") String mbid); }
La clase se ha implementado como un singleton ya que podemos utilizar siempre la misma instancia, de ahí que el constructor sea privado. Además en este constructor vamos a instanciar el repositorio de Retrofit ya que podemos utilizar siempre la misma instancia.
package com.danielme.android.dagger2; import android.content.Context; import java.io.IOException; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class SingletonServiceImpl implements SingletonService { private static final String URL = "http://musicbrainz.org/ws/2/"; private static final SingletonServiceImpl instance = new SingletonServiceImpl(); private final ArtistRepository artistRepository; public static SingletonServiceImpl getInstance(){ return instance; } private SingletonServiceImpl() { Retrofit retrofit = new Retrofit.Builder() .baseUrl(URL) .addConverterFactory(GsonConverterFactory.create()) .build(); artistRepository = retrofit.create(ArtistRepository.class); } @Override public String getPath(Context context) { return context.getApplicationContext().getCacheDir().getAbsolutePath(); } @Override public Artist getArtist(String mbid) throws IOException { return artistRepository.getArtist(mbid).execute().body(); } }
Si ejecutamos la aplicación veremos el path del directorio para caché que tenemos asignado y el nombre del artista obtenido desde MusicBrainz.
Aplicando DI con dagger 2
Vamos a hacer que las dependencias de la Activity y SingletonService sean inyectadas por Dagger 2. Al principio utilizar Dagger parece más complicado de lo que realmente es ya que es necesario escribir algo de código y generar varias clases. Además en Android tenemos la dificultad adicional que implica el hecho de que las activities sean instanciadas por el propio sistema por lo que no pueden ser creadas por Dagger.
Para añadir Dagger 2 al proyecto seguimos las instrucciones de la página oficial, a continuación se muestran los cambios realizados en el app.build de nuestro proyecto de ejemplo
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion '25.0.2' defaultConfig { applicationId "com.danielme.android.dagger" minSdkVersion 16 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.google.dagger:dagger:2.11' annotationProcessor 'com.google.dagger:dagger-compiler:2.11' compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' }
Esta configuración es válida para Dagger 2.11 con Gradle 2.3.0, pero para versiones de Gradle más antiguas es necesario utilizar el plugin android-apt tal y como se hacía en la primera versión de este tutorial. El app.build de esa primera versión se puede consultar en GitHub.
Primero creamos los módulos, clases que instanciarán y proveerán las dependencias que queremos inyectar y/o las clases que consumen esas dependencias. En nuestro ejemplo primero vamos a refactorizar la clase SingletonServiceImpl para poder «inyectar» a través de su constructor las dos dependencias que queremos recibir en el mismo. Ya no necesitamos crear un Singleton tal y como veremos más adelante. Tampoco tenemos que preocuparnos de crear el repositorio de Retrofit, es una dependencia que Dagger gestionará por nosotros.
package com.danielme.android.dagger2.service; import android.content.Context; import com.danielme.android.dagger2.model.Artist; import com.danielme.android.dagger2.repository.ArtistRepository; import java.io.IOException; public class SingletonServiceImpl implements SingletonService { private final ArtistRepository artistRepository; private final Context context; public SingletonServiceImpl(ArtistRepository artistRepository, Context context) { this.artistRepository = artistRepository; this.context = context; } @Override public String getPath() { return context.getApplicationContext().getCacheDir().getAbsolutePath(); } @Override public Artist getArtist(String mbid) throws IOException { return artistRepository.getArtist(mbid).execute().body(); } }
Ahora definimos el módulo. Aunque nuestro proyecto es muy sencillo, vamos a crear tres módulos ya que es una buena práctica tener un módulo por cada capa de la aplicación. Cada módulo no es más que una clase POJO anotada con @Module con métodos que instancian las dependencias que gestiona el módulo. Estos métodos, que suelen nombrarse utilizando el prefijo provides, se anotan con @Provides y con el scope o ámbito de vida de la dependencia que estamos creando. En nuestro caso todos serán singleton, si queremos otro scope tendremos que implementarlo nosotros.
En RetrofitModule se crea el resource con Retrofit, utilizando la lógica que sea necesaria.
package com.danielme.android.dagger2.repository; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @Module public class RetrofitModule { private static final String URL = "http://musicbrainz.org/ws/2/"; @Provides @Singleton ArtistRepository providesArtistRepository() { Retrofit retrofit = new Retrofit.Builder() .baseUrl(URL) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(ArtistRepository.class); } }
En ServiceModule vamos a crear una instancia de la interfaz SingletonService con la implementación SingletonServiceImpl. Las dependencias de Dagger que necesitamos inyectar a través del constructor simplemente las pasamos como parámetros al método provider.
package com.danielme.android.dagger2.service; import android.content.Context; import com.danielme.android.dagger2.repository.ArtistRepository; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; @Module public class ServiceModule { @Provides @Singleton SingletonService providesSingletonServiceImpl(ArtistRepository artistRepository, Context context) { return new SingletonServiceImpl(artistRepository, context); } }
Por último, creamos un módulo para proveer objetos del propio Android, como por ejemplo las SharedPreferences, servicios del sistema, el contexto, etc, que queremos inyectar. Puesto que estos objetos son gestionados por Android y no los crea Dagger, en el constructor del módulo vamos a recibir el propio contexto.
package com.danielme.android.dagger2; import android.content.Context; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; @Module public class AppModule { private final Context context; public AppModule(Context context) { this.context = context; } @Provides @Singleton Context providesContext() { return context; } }
Para la definición de clases concretas cuya instanciación simplemente requiere llamar al constructor no es necesario definir un provider ya que podemos hacerlo directamente mediante anotaciones.
@Singleton public class Dependencia { @Inject public Dependencia(Clase1 clase1) { this.clase1 = claase1; }
También existe una forma más simple de definir providers de implementaciones de interfaces:
@Module public abstract class ServiceModule { @Binds public abstract SingletonService providesSingletonService(SingletonServiceImpl singletonService); }
En lugar de un método @Provides se define un método abstracto anotado con @Bind que devuelve la interfaz y recibe una implementación de la misma (esto obliga a que la clase sea abstracta). La implementación y sus dependencias se definen con anotaciones tal y como acabamos de ver, para el ejemplo quedaría tal que así:
@Singleton public class SingletonServiceImpl implements SingletonService { private final ArtistRepository artistRepository; private final Context context; @Inject public SingletonServiceImpl(ArtistRepository artistRepository, Context context) { this.artistRepository = artistRepository; this.context = context; }
Ahora definimos con una interfaz la clase «inyectora» denominada component responsable de asignar las referencias de las dependencias en nuestras activities, servicios y fragments. Indicaremos los módulos a los que tiene acceso el componente y definimos un método inject para cada Activity, Fragment o Service en el que haya que inyectar dependencias. Hay que crear un método inject para cada una de estas clases en concreto, no podemos utilizar una clase padre de las mismas.
package com.danielme.android.dagger2; import com.danielme.android.dagger2.repository.RetrofitModule; import com.danielme.android.dagger2.service.ServiceModule; import com.danielme.android.dagger2.view.MainActivity; import javax.inject.Singleton; import dagger.Component; @Singleton @Component(modules = {AppModule.class, ServiceModule.class, RetrofitModule.class}) public interface AppComponent { void inject(MainActivity activity); }
El component lo vamos a instanciar una única vez nada más iniciarse la aplicación, esto se puede conseguir especializando la clase Application. Aquí también construimos nuestro AppModule con el contexto de la aplicación.
package com.danielme.android.dagger2; import android.app.Application; public class DaggerDemoApplication extends Application { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); } public AppComponent getAppComponent() { return appComponent; } }
No podemos olvidarnos de establecer esta clase como el Application en el manifest.
<application android:allowBackup="true" android:name=".DaggerDemoApplication" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >
Este código no va a compilar porque la clase DaggerAppComponent no existe. Esta clase es la implementación de la interfaz AppComponent que Dagger debe generar y que se corresponde con el nombre del component más el prefijo «Dagger». Por tanto, hacemos un «Build->Rebuild project» y si todo va bien se generará la clase DaggerAppComponent.
El último paso es realizar la inyección de dependencias en MainActivity. Definimos las dependencias, en nuestro caso una implementación de la interfaz SingletonService, en un atributo no privado anotado con Inject e invocamos la inyección en el onCreate.
public class MainActivity extends AppCompatActivity { @Inject SingletonService singletonService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((DaggerDemoApplication) getApplication()).getAppComponent().inject(this); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ((TextView) findViewById(R.id.textView1)).setText(singletonService.getPath()); (new ArtistTask()).execute(); }
Un hecho interesante de este código es que MainActivity utiliza el contrato definido por la interfaz SingeltonService y gracias a Dagger no instancia ninguna implementación de la misma. Hemos eliminado por tanto todo acoplamiento posible entre la capa de vista y de servicios, cumpliéndose los principios de inversión de la dependencia y de sustitución de Liskov de los principios SOLID.
El siguiente diagrama de clases muestra la aplicación de ejemplo tras estos cambios (pulsar para ampliar).
Dagger 2 está diseñado para ser lo más eficiente posible de ahí que no utilice reflection y se base en clases generadas de forma automática en tiempo de compilación de las que sólo hemos tenido que utilizar de forma explícita en nuestro código la implementación del Component (DaggerAppComponent). Si queremos echar un vistazo a estas clases, que nunca debemos modificar, las tenemos en la ruta /app/build/generated/source/apt/.
Múltiples implementaciones
Una situación que no se produce en nuestro ejemplo pero que probablemente en algún momento nos encontremos con ella es que necesitemos crear más de una dependencia distinta pero del mismo tipo, por ejemplo dos implementaciones distintas de una misma interfaz. Dagger 2 proporciona una implentación de la anotación @Named para asignar un nombre único e identificativo a cada dependencia y poder distinguirlas a la hora de realizar las inyecciones. Por ejemplo:
@Provides @Named("service_A") @Singleton SingletonService providesSingletonServiceAImpl() { return new SingletonServiceAImpl(); } @Provides @Named("service_B") @Singleton SingletonService providesSingletonServiceBImpl() { return new SingletonServiceBImpl(); }
Al realizar la inyección volvemos a utilizar la anotación @Named para indicar la dependencia que queremos.
@Inject @Named("service_A") SingletonService singletonService;
Proyecto de ejemplo
El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.