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:
-
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.
-
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
-
Instalar
AndroidInjectionModule
en su componente de la aplicación para asegurarque todos los enlaces necesarios para estos tipos de base están disponibles. -
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> {}}
-
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óduloabstract
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();
-
A continuación, haz que tu
Application
implementeHasAndroidInjector
y@Inject
unDispatchingAndroidInjector<Object>
retorno del métodoandroidInjector()
: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; }}
-
Por último, en tu método
Activity.onCreate()
, llama aAndroidInjection.inject(this)
antes de llamar asuper.onCreate();
:public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
-
¡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 Fragment
s en onAttach()
.
A diferencia de los módulos definidos para los Activity
s, puede elegir dónde instalar los módulos para los Fragment
s. 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 Fragment
necesita 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
yDaggerIntentService
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 javac
se asegurará de que no se referencie ningún campo antes de que se haya establecido, lo que ayuda a evitarNullPointerException
s. Cuando se requiere la inyección de miembros (como se discutióanteriormente), se prefiere inyectar tan pronto como sea posible. Por esta razón, DaggerActivity
llama 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 Fragment
s de la instancia de actividad anterior durante el cambio de configuración, que a su vez inyecta los Fragment
s. Para que la inyección de Fragment
tenga éxito, el Activity
debe 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.