El anterior artículo Eligiendo IDE para trabajar con Android, fue el primero desde que estoy colaborando en mi empresa en un proyecto de Android. En este artículo vamos a ver como podemos llevar a cabo inyección de dependencias en Android utilizando Dagger.
Introducción a Dagger
Dagger es un inyector de dependencias diseñado para dispositivos de gama baja porque no utiliza reflexión para crear e injectar las dependencias ya que esta técnica consume tiempo y memoria, sobre todo en dispositivos antiguos. Dagger utiliza un pre-compilador que genera las clases que necesita sin necesidad de reflexión.Para centrarnos en lo esencial de Dagger vamos a ver una ejemplo de una app sencilla donde habrá cosas que no hariamos en una aplicación real pero que nos servirá para el objetivo de este artículo. Se trata de una app que se va a conectar a una api rest que devuelve información de paises, la url es http://restcountries.eu/rest/v1/all, y escribirá el contenido en un ListView sencillo.
/
Bases del ejemplo
Primero vamos a ver las partes de nuestro ejemplo sin utilizar Dagger, pero iremos enfocando la aplicación para que tenga sentido inyección de dependencias..Primero vamos a tener un layout para la ventana principal donde vamos a tener un ListView donde representar los paises.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <ListView android:id="@+id/countriesList" android:layout_width="match_parent" android:layout_height="match_parent"></ListView> </RelativeLayout>
Y un layout para cada pais.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="2dp" android:layout_margin="2dp" android:background="@android:color/darker_gray"> <TextView android:id="@+id/nameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_gravity="center" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text=" - "/> <TextView android:id="@+id/capitalNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" /> </LinearLayout>
Para comunicarnos con la api vamos a utilizar la librería OkHttp de Square, pero no vamos a utilizar OkHttp directamente en el activity sino que vamos a crear una clase que se encargue de encapsular el uso de OkHttp y esta clase a su vez la vamos a abstraer en un interface, de esta forma el activity va a dependender de una asbracción y no de una clase concreta, lo que nos va a permitir en un futuro cambiar OkHttp por otra librería para realizar peticiones Http como por ejemplo Volley, el impacto del cambio en la aplicación sería mínimo. Si en lugar de ser una aplicación de un activity es algo más real pues la ganancia es mucho mayor. Además esta estructura nos va a permitir tener un fake que sustituya a la encapsulación de OkHttp y asi no necesitar de red para poder hacer pruebas o cambios en el diseño de la aplicación. Y lo que es más importante, con esta estructura luego tendrá sentido inyectar la dependencia de la abstracción.
Entonces por una lado tenemos el interface de la clase que encapsula la comunicación con la api rest
public interface RestCountriesClient { public void Get(ResponseHandler<List<Country>> handler); }
La implementación de esta interfaz utilizando OkHttp, donde parseamos el json a nuestro modelo Country.
public class DefaultRestCountriesClient implements RestCountriesClient{ private final OkHttpClient client = new OkHttpClient(); public void Get(final ResponseHandler<List<Country>> handler) { Request request = new Request.Builder() .url("http://restcountries.eu/rest/v1/all") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { if (handler != null) handler.onFailure(e.getMessage()); } @Override public void onResponse(Response response) throws IOException { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); if (handler != null) { Gson gson = new Gson(); Country[] countries = gson.fromJson(response.body().string(), Country[].class); handler.onResponse(Arrays.asList(countries)); } } }); } }
Un handler para manejar la respuesta de la api rest en el activity, porque la comunicación es asíncrona.
public interface RestCountriesClient { public void Get(ResponseHandler<List<Country>> handler); }
Un adapter para escribir cada fila del layout, vamos a usar el patrón de diseño View Holder recomendado por Android Developers
public class CountryItemAdapter extends BaseAdapter { private Context context; private List<Country> items; public CountryItemAdapter(Context context,List<Country> items){ this.context = context; this.items = items; } @Override public int getCount() { return items.size(); } @Override public Object getItem(int i) { return items.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View convertView, ViewGroup viewGroup) { View row = convertView; ViewHolder holder = null; if (row == null) { //retrieve item view row = LayoutInflater.from(context) .inflate(R.layout.item_country, null, false); holder = new ViewHolder(); holder.nameTextView = (TextView) row.findViewById(R.id.nameTextView); holder.capitalTextView = (TextView) row.findViewById(R.id.capitalNameTextView); row.setTag(holder); } else { holder = (ViewHolder) row.getTag(); } //retrieve item by position final Country item = items.get(i); holder.nameTextView.setText(item.getName()); holder.capitalTextView.setText(item.getCapital()); return row; } /** * The view holder design pattern prevents using findViewById() * repeatedly in the getView() method of the adapter. * * http://developer.android.com/training/improving-layouts/smooth-scrolling.html#ViewHolder */ static class ViewHolder { TextView nameTextView; TextView capitalTextView; } }
Y nos falta el activity con la dependencia del cliente de la api rest, de momento depende de la clase concreta porque el mismo se encarga de instanciarla.
public class MainActivity extends Activity { RestCountriesClient restCountriesClient = new DefaultRestCountriesClient(); Context context; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.countries_main); context = getApplicationContext(); final List<Country> countries= new ArrayList<Country>(); final CountryItemAdapter adapter = new CountryItemAdapter(this,countries); ListView gridView = (ListView) findViewById(R.id.countriesList); gridView.setAdapter(adapter); restCountriesClient.Get(new ResponseHandler<List<Country>>(){ @Override public void onFailure(String ErrorMessage) { Toast.makeText(context,ErrorMessage, Toast.LENGTH_SHORT); } @Override public void onResponse(final List<Country> response) { runOnUiThread(new Runnable() { @Override public void run() { for (Country country:response){ countries.add(country); } adapter.notifyDataSetChanged(); } }); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
Y este es el resultado de nuestro ejemplo de momento sin inyección de dependencias.
Ya tenemos la estructura necesaria para hacer inyección de dependencias.
Marcar dependencias con la anotación @Inject
Lo primero es que el activity ya no va a crear el cliente rest directamente, sino que solo define la dependencia del interface decorada con la anotación @Inject, introducida en el estándar JSR 330.//RestCountriesClient restCountriesClient = new DefaultRestCountriesClient(); @Inject RestCountriesClient restCountriesClient;
Crear el módulo que provee las dependencias
Ahora tenemos que crear un módulo, como se llama en Dagger, que es la clase encargada de crear las proveer las dependencias.@Module(injects = MainActivity.class) public class ApplicationDIModule { @Provides @Singleton public RestCountriesClient providesRestCountriesClient() { return new DefaultRestCountriesClient(); } }
Aquí nos tenemos que fijar en varias cosas. La primera es que el module debe de ser decorado con la anotación de Dagger @Module. Por convención las funciones del módulo que devuelven dependencias empiezan con provides, deben tener la anotación @Provides y si queremos que la función siempre nos devuelva el mismo objeto también debe llevar la anotación @Singleton. También es necesario indicarle al módulo cuales son las clases a las que puede proveer dependencias, en nuestro caso el MainActivity, pero podrían ser más.
Crear el ObjectGraph en base a módulos
Ahora necesitamos crear en el punto de entrada de la aplicación el objeto ObjectGraph de Dagger en base al módulo que provee las dependencias, para conseguirlo creamos una clase App que herede de Application, sobreescribimos el método onCreate para crear el ObjectGrap y añadimos una propiedad para poder acceder después a él. Deberemos dar valor a la propiedad name de applicatión en el manifiesto haciendo referencia a nuestra clases app.public class App extends android.app.Application { private ObjectGraph objectGraph; @Override public void onCreate() { super.onCreate(); objectGraph = ObjectGraph.create(new ApplicationDIModule()); } public ObjectGraph getObjectGraph() { return objectGraph; } }
Injectar dependencias a un objeto
Y por último hay que inyectar la dependencia al Activity, esta es la parte que menos me gusta de la inyección de dependencias en Android, el activity es quien debe solicitar que le inyecten sus dependencias accediendo a la propiedad getObjectGraph de la clase application que hemos creado antes. Esto en .Net no funciona así con los contenedores IOC, es decir, el punto mas alto de la jerarquia, en este caso el Activity pero en .Net podría ser un controller de mvc o web api, no solicita la inyección, para el es trasparente quien lo hace. Debido a esta limitación, que entiendo que se debe a como esta montado el ciclo de vida de los Activities, lo que si que podemos hacer es crearnos una clase base para los activites, o los fragments, según este diseñada nuestra aplicación.public abstract class BaseActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((App) getApplication()).getObjectGraph().inject(this); } }
Código disponible en GitHub.
Libros relacionados
Android Programming: The Big Nerd Ranch GuideAndroid 4.4 App Development Essentials
Hola Jorge muy bueno el post!! Una cuestión, estaba pensando en aplicar lo que decías de poder reemplazar el módulo de OkHttp por otro, y pensé en Retrofit. Y ahora veo que Retrofit funciona con un flujo y modularidad idéntico al que tu describes, salvo en la inyección de dependencia... ¿Merece la pena en este caso hacerlo? Un saludo.
ResponderEliminarMuy buena explicación! Gracias por tu tiempo :)
ResponderEliminar