Dagger y Android

Una de las principales ventajas de Dagger 2 sobre la mayoría de los otros marcos de inyección de dependencia es que su implementación estrictamente generada (sin reflexión) significa que puede ser utilizada en aplicaciones Android. Sin embargo, todavía hay que hacer algunas consideraciones cuando se utiliza Dagger dentro de las aplicaciones de Android.

Filosofía

Aunque el código escrito para Android es de origen Java, a menudo es bastante diferente en términos de estilo. Típicamente, tales diferencias existen para acomodar las consideraciones de rendimiento únicas de una plataforma móvil.

Pero muchos de los patrones comúnmente aplicados al código destinado a Android son contrarios a los aplicados a otro código Java. Incluso muchos de los consejos de Effective Java se consideran inapropiados para Android.

Para lograr los objetivos de código idiomático y portátil, Dagger se basa en ProGuard para post-procesar el código de bytes compilado. Esto permite a Dagger emitir código fuente que se ve y se siente natural tanto en el servidor como en Android, mientras que utiliza las diferentes cadenas de herramientas para producir código de bytes que se ejecuta eficientemente en ambos entornos. Además, Dagger tiene el objetivo explícito de asegurar que el código fuente Java que genera es compatible con las optimizaciones de ProGuard.

Por supuesto, no todos los problemas se pueden abordar de esa manera, pero es el principal mecanismo por el que se proporcionará la compatibilidad específica de Android.

tl;dr

Dagger asume que los usuarios de Android usarán R8 o ProGuard.

Por qué Dagger en Android es difícil

Una de las dificultades centrales de escribir una aplicación Android usando Dagger es que muchas clases del framework de Android son instanciadas por el propio SO, comoActivity y Fragment, pero Dagger funciona mejor si puede crear todos los objetosinjectados. En cambio, tiene que realizar la inyección de miembros en un método del ciclo de vida. Esto significa que muchas clases terminan pareciendo:

public class FrombulationActivity extends Activity { @Inject Frombulator frombulator; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // DO THIS FIRST. Otherwise frombulator might be null! ((SomeApplicationBaseType) getContext().getApplicationContext()) .getApplicationComponent() .newActivityComponentBuilder() .activity(this) .build() .inject(this); // ... now you can write the exciting code }}

Esto tiene algunos problemas:

  1. Copiar y pegar el código hace que sea difícil de refactorizar más tarde. A medida que más y más desarrolladores copien-peguen ese bloque, menos sabrán lo que realmente hace.

  2. Más fundamentalmente, requiere que el tipo que solicita la inyección(FrombulationActivity) conozca su inyector. Incluso si esto se hace a través de interfaces en lugar de tipos concretos, se rompe un principio básico de la inyección de dependencia: una clase no debe saber nada acerca de cómo se inyecta.

dagger.android

Las clases en dagger.android ofrecen un enfoque para simplificar el aboveproblems. Esto requiere el aprendizaje de algunas APIs y conceptos adicionales, pero le da una reducción de la placa base y la inyección en sus clases de Android en el lugar correcto en el ciclo de vida.

Otro enfoque es simplemente utilizar las APIs normales de Dagger y seguir las guías como la de aquí.Esto puede ser más simple de entender, pero viene con la desventaja de tener que escribir manualmente boilerplate extra.

Los equipos de Jetpack y Dagger están trabajando juntos en una nueva iniciativa para Dagger en Android que espera ser un gran cambio de la statusquo actual. Mientras que por desgracia no está listo todavía, esto puede ser algo a considerarcuando la elección de cómo utilizar Dagger en sus proyectos de Android hoy.

La inyección de objetos de actividad

  1. Instalar AndroidInjectionModule en su componente de la aplicación para asegurarque todos los enlaces necesarios para estos tipos de base están disponibles.

  2. Empiece escribiendo un @Subcomponent que implementeAndroidInjector<YourActivity>, con un@Subcomponent.Factory que extiendaAndroidInjector.Factory<YourActivity>:

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. Después de definir el subcomponente, añádelo a tu jerarquía de componentes definiendo un módulo que vincule la fábrica del subcomponente y añadiéndolo al componente que inyecta tu Application:

    @Module(subcomponents = YourActivitySubcomponent.class)abstract class YourActivityModule { @Binds @IntoMap @ClassKey(YourActivity.class) abstract AndroidInjector.Factory<?> bindYourAndroidInjectorFactory(YourActivitySubcomponent.Factory factory);}@Component(modules = {..., YourActivityModule.class})interface YourApplicationComponent { void inject(YourApplication application);}

    Pro-tip: Si tu subcomponente y su fábrica no tienen otros métodos osupertipos que los mencionados en el paso #2, puedes usar@ContributesAndroidInjector para generarlos por ti. En lugar de los pasos 2 y 3, añada un método de módulo abstract que devuelva su actividad, anótelo con @ContributesAndroidInjector y especifique los módulos que desea instalar en el subcomponente. Si el subcomponente necesita ámbitos, aplique también las anotaciones de ámbito al método.

    @ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
  4. A continuación, haz que tu Application implemente HasAndroidInjectory @Inject unDispatchingAndroidInjector<Object> retorno del método androidInjector():

    public class YourApplication extends Application implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector; @Override public void onCreate() { super.onCreate(); DaggerYourApplicationComponent.create() .inject(this); } @Override public AndroidInjector<Object> androidInjector() { return dispatchingAndroidInjector; }}
  5. Por último, en tu método Activity.onCreate(), llama aAndroidInjection.inject(this)antes de llamar a super.onCreate();:

    public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
  6. ¡Felicidades!

¿Cómo ha funcionado?

AndroidInjection.inject() obtiene un DispatchingAndroidInjector<Object> de la Application y pasa su actividad a inject(Activity). El DispatchingAndroidInjector busca el AndroidInjector.Factory para la clase de su actividad (que es YourActivitySubcomponent.Factory), crea el AndroidInjector (que es YourActivitySubcomponent), y pasa su actividad a inject(YourActivity).

Inyectar objetos Fragment

Inyectar un Fragment es tan simple como inyectar un Activity. Defina su subcomponente de la misma manera.

En lugar de inyectar en onCreate() como se hace para los tipos Activity, inyecte los Fragments en onAttach().

A diferencia de los módulos definidos para los Activitys, puede elegir dónde instalar los módulos para los Fragments. Puede hacer que su componente Fragment sea un subcomponente de otro componente Fragment, un componente Activity o el componenteApplication – todo depende de qué otros enlaces requiera su Fragment. Después de decidir la ubicación del componente, haga que el tipo correspondiente implemente HasAndroidInjector (si no lo hace ya). Por ejemplo, si su Fragmentnecesita enlaces de YourActivitySubcomponent, su código se verá algo así:

public class YourActivity extends Activity implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> androidInjector; @Override public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); // ... } @Override public AndroidInjector<Object> androidInjector() { return androidInjector; }}public class YourFragment extends Fragment { @Inject SomeDependency someDep; @Override public void onAttach(Activity activity) { AndroidInjection.inject(this); super.onAttach(activity); // ... }}@Subcomponent(modules = ...)public interface YourFragmentSubcomponent extends AndroidInjector<YourFragment> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourFragment> {}}@Module(subcomponents = YourFragmentSubcomponent.class)abstract class YourFragmentModule { @Binds @IntoMap @ClassKey(YourFragment.class) abstract AndroidInjector.Factory<?> bindYourFragmentInjectorFactory(YourFragmentSubcomponent.Factory factory);}@Subcomponent(modules = { YourFragmentModule.class, ... }public interface YourActivityOrYourApplicationComponent { ... }

Tipos de la estructura base

Debido a que DispatchingAndroidInjector busca el apropiadoAndroidInjector.Factory por la clase en tiempo de ejecución, una clase base puede implementarHasAndroidInjector así como llamar a AndroidInjection.inject(). Todo lo que tiene que hacer cada subclase es enlazar un @Subcomponent correspondiente. Dagger proporciona algunos tipos base que hacen esto, como DaggerActivity y DaggerFragment, si no se tiene una jerarquía de clases complicada. Dagger también proporciona un DaggerApplication para el mismo propósito – todo lo que necesitas hacer es extenderlo y anular el método applicationInjector() para devolver el componente que debe inyectar el Application.

También se incluyen los siguientes tipos:

  • DaggerService y DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

Nota: DaggerBroadcastReceiver sólo debe utilizarse cuando elBroadcastReceiver está registrado en el AndroidManifest.xml. Cuando elBroadcastReceiver se crea en tu propio código, prefiere la inyección del constructor en su lugar.

Librerías de soporte

Para los usuarios de la biblioteca de soporte de Android, los tipos paralelos existen en el paquetedagger.android.support.

TODO(ronshapiro): deberíamos empezar a dividir esto por paquetes androidx

¿Cómo lo consigo?

Añade lo siguiente a tu build.gradle:

dependencies { implementation 'com.google.dagger:dagger-android:2.x' implementation 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries annotationProcessor 'com.google.dagger:dagger-android-processor:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x'}

Cuándo inyectar

Se prefiere la inyección de constructores siempre que sea posible porque javacse asegurará de que no se referencie ningún campo antes de que se haya establecido, lo que ayuda a evitarNullPointerExceptions. Cuando se requiere la inyección de miembros (como se discutióanteriormente), se prefiere inyectar tan pronto como sea posible. Por esta razón, DaggerActivityllama a AndroidInjection.inject() inmediatamente en onCreate(), antes de llamar asuper.onCreate(), y DaggerFragment hace lo mismo en onAttach(), lo que también evita inconsistencias si el Fragment se vuelve a adjuntar.

Es crucial llamar a AndroidInjection.inject() antes de super.onCreate() en un Activity, ya que la llamada a super adjunta Fragments de la instancia de actividad anterior durante el cambio de configuración, que a su vez inyecta los Fragments. Para que la inyección de Fragment tenga éxito, el Activitydebe estar ya inyectado. Para los usuarios de ErrorProne, es un error de compilador para llamar a AndroidInjection.inject() después de super.onCreate().

FAQ

Scoping AndroidInjector.Factory

AndroidInjector.Factory está destinado a ser una interfaz sin estado para que los implementadores no tienen que preocuparse por la gestión del estado relacionado con el objeto que se inyecta. Cuando DispatchingAndroidInjector solicita unAndroidInjector.Factory, lo hace a través de un Provider para que no retenga textualmente ninguna instancia de la fábrica. Debido a que algunas implementaciones pueden retener una instancia de la Activity/Fragment/etc que se está inyectando, es un error en tiempo de compilación aplicar un ámbito a los métodos que los proporcionan. Si está seguro de que su AndroidInjector.Factory no retiene una instancia al objeto inyectado, puede suprimir este error aplicando@SuppressWarnings("dagger.android.ScopedInjectorFactory") a su método del módulo.