Cómo usar Dagger en apps para Android

En la página Conceptos básicos de Dagger, se explica cómo puede ayudarte esta herramienta a automatizar la inserción de dependencias en tu app. Con Dagger, no es necesario escribir código estándar pesado y propenso a errores.

Resumen de prácticas recomendadas

  • Usa la inyección de constructor con @Inject para agregar tipos al grafo de Dagger siempre que sea posible. Cuando no sea posible:
    • Usa @Binds para indicarle a Dagger qué implementación debería tener una interfaz.
    • Usa @Provides para indicarle a Dagger cómo proporcionar clases que no sean de propiedad de tu proyecto.
  • Solo debes declarar los módulos una vez en un componente.
  • Nombra las anotaciones de alcance según el ciclo de vida en la ubicación donde se usa la anotación. Entre los ejemplos, se incluyen @ApplicationScope, @LoggedUserScope y @ActivityScope.

Cómo agregar dependencias

Para usar Dagger en tu proyecto, agrega estas dependencias a tu aplicación en el archivo build.gradle. La versión más reciente de Dagger se encuentra en este proyecto de GitHub.

Kotlin

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Java

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Dagger en Android

Considera una app para Android de ejemplo con el grafo de dependencia de la figura 1.

LoginActivity depende de LoginViewModel, que, a su vez, depende de UserRepository, y este depende de UserLocalDataSource y UserRemoteDataSource, los cuales dependen de Retrofit.

Figura 1: Grafo de dependencia del código de ejemplo

En Android, sueles crear un grafo de Dagger que reside en tu clase de aplicación porque deseas que una instancia del grafo permanezca en la memoria mientras se ejecuta la app. De esta manera, se adjunta el grafo al ciclo de vida de la app. En algunos casos, es posible que también desees que el contexto de la aplicación esté disponible en el grafo. Para eso también necesitarías que el grafo esté en la clase Application. Una de las ventajas de este enfoque es que el grafo está disponible para otras clases del framework de Android. Además simplifica las pruebas, ya que te permite usar una clase Application personalizada en ellas.

Debido a que la interfaz que genera el grafo está anotada con @Component, puedes llamarla ApplicationComponent o ApplicationGraph. Por lo general, mantendrás una instancia de ese componente en tu clase Application personalizada y la llamarás cada vez que necesites el grafo de la aplicación, como se muestra en el siguiente fragmento de código:

Kotlin

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

Java

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

Debido a que el sistema crea instancias de ciertas clases de framework de Android, como actividades y fragmentos, Dagger no puede crearlas por ti. Para actividades específicas, todo el código de inicialización debe incluirse en el método onCreate(). Eso significa que no puedes usar la anotación @Inject en el constructor de la clase (inyección de constructor) como hiciste en los ejemplos anteriores. En su lugar, debes usar la inserción de campo.

En lugar de crear las dependencias que requiere una actividad del método onCreate(), te conviene que Dagger rellene esas dependencias por ti. Para la inyección de campo, en cambio, aplica la anotación @Inject en los campos que deseas obtener del grafo de Dagger.

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;
}

Para simplificar, LoginViewModel no es un ViewModel de componentes de la arquitectura de Android; es solo una clase normal que actúa como un ViewModel. Para obtener más información sobre cómo inyectar estas clases, consulta el código de la implementación oficial de Android Blueprints Dagger, en la rama dev-dagger.

Con relación a Dagger, debes tener en cuenta que los campos inyectados no pueden ser privados. Como mínimo, deben tener visibilidad de paquete privado, como en el código anterior.

Actividades de inserción

Dagger necesita saber que LoginActivity debe acceder al grafo para proporcionar el ViewModel que requiere. En la página Conceptos básicos de Dagger, usaste la interfaz @Component para obtener objetos del grafo exponiendo funciones con el tipo de datos que se muestra para lo que deseas obtener del grafo. En este caso, debes informar a Dagger sobre un objeto (en este caso, LoginActivity) que requiere la inyección de una dependencia. Para eso, expones una función que tome como parámetro el objeto que solicita la inyección.

Kotlin

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

Esta función le indica a Dagger que LoginActivity quiere acceder al grafo y solicita la inyección. Dagger debe satisfacer todas las dependencias que requiere LoginActivity (LoginViewModel con sus propias dependencias). Si tienes varias clases que solicitan inyección, debes declararlas todas específicamente en el componente con su tipo exacto. Por ejemplo, si LoginActivity y RegistrationActivity solicitan la inyección, habrá dos métodos inject() en lugar de uno genérico que abarque ambos casos. El método inject() genérico no le indica a Dagger qué se debe proporcionar. Las funciones que se incluyen en la interfaz pueden tener cualquier nombre, pero, por convención, en Dagger se suelen llamar inject() cuando reciben el objeto para insertar como un parámetro.

Para inyectar un objeto en la actividad, deberías usar el appComponent definido en tu clase Application y llamar al método inject(), pasando una instancia de la actividad que solicita la inserción.

A fin de evitar problemas relacionados con el restablecimiento de fragmentos, cuando uses actividades, inserta Dagger en el método onCreate() de la actividad antes de llamar a super.onCreate(). Durante la fase de restablecimiento en super.onCreate(), una actividad agrega fragmentos que pueden requerir acceso a las vinculaciones de la actividad.

Cuando uses fragmentos, inyecta Dagger en el método onAttach() del fragmento. En este caso, la inyección se puede hacer antes o después de llamar a super.onAttach().

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Indiquémosle a Dagger cómo proporcionar el resto de las dependencias para compilar el grafo:

Kotlin

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

Módulos de Dagger

Para este ejemplo, usas la biblioteca de herramientas de redes de Retrofit. UserRemoteDataSource tiene una dependencia en LoginRetrofitService. Sin embargo, la manera de crear una instancia de LoginRetrofitService es diferente de la manera en que lo hiciste hasta ahora. No implica crear una instancia de clase; es el resultado de llamar a Retrofit.Builder() y pasar parámetros diferentes para configurar el servicio de acceso.

Además de la anotación @Inject, hay otra manera de indicarle a Dagger cómo proporcionar una instancia de una clase: la información incluida en los módulos de Dagger. Un módulo de Dagger es una clase con anotaciones @Module. En él, puedes definir dependencias con la anotación @Provides.

Kotlin

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

Java

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

Los módulos son una forma de encapsular semánticamente información sobre cómo proporcionar objetos. Como puedes ver, se llamó a la clase NetworkModule a fin de agrupar la lógica necesaria para proporcionar objetos relacionados con las herramientas de redes. Si se expande la aplicación, también puedes agregar la forma de proporcionar un OkHttpClient aquí, o de configurar Gson o Moshi.

Las dependencias de un método @Provides son los parámetros de ese método. Para el método anterior, se puede proporcionar LoginRetrofitService sin dependencias porque el método no tiene parámetros. Si declaraste un OkHttpClient como parámetro, Dagger deberá proporcionar una instancia de OkHttpClient del grafo a fin de satisfacer las dependencias de LoginRetrofitService. Por ejemplo:

Kotlin

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Java

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

Para que el grafo de Dagger sepa que existe este módulo, debes agregarlo a la interfaz @Component de la siguiente manera:

Kotlin

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Java

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

Para agregar tipos al grafo de Dagger, se recomienda usar la inyección de constructor (es decir, con la anotación @Inject en el constructor de la clase), lo cual no siempre es posible, por lo que debes usar módulos de Dagger. Por ejemplo, esto sucede cuando quieres que Dagger use el resultado de un cálculo para determinar cómo crear la instancia de un objeto. Cada vez que Dagger tiene que proporcionar una instancia de ese tipo, ejecuta el código dentro del método @Provides.

Así es como se ve el grafo de Dagger en el ejemplo en este momento:

Diagrama del grafo de la dependencia LoginActivity

Figura 2: Representación del grafo cuando Dagger inyecta LoginActivity

El punto de entrada al grafo es LoginActivity. Debido a que LoginActivity inyecta LoginViewModel, Dagger crea un grafo que sabe cómo proporcionar una instancia de LoginViewModel y, de forma recurrente, de sus dependencias. Dagger sabe cómo hacerlo debido a la anotación @Inject incluida en el constructor de las clases.

Dentro del ApplicationComponent generado por Dagger, hay un método de tipo de fábrica para obtener instancias de todas las clases que sabe cómo proporcionar. En este ejemplo, Dagger delega al NetworkModule incluido en ApplicationComponent la obtención de una instancia de LoginRetrofitService.

Alcances de Dagger

Los alcances se mencionaron en la página Conceptos básicos de Dagger; son una forma de contar con una instancia única de un tipo en un componente. Eso es lo que significa determinar el alcance de un tipo en relación con el ciclo de vida del componente.

Debido a que tal vez quieras usar UserRepository en otras funciones de la app sin tener que crear un objeto nuevo cada vez, puedes designarlo como instancia única para toda la app. Lo mismo sucede con LoginRetrofitService: además de que puede ser costoso crearlo, te conviene que se reutilice una instancia única de ese objeto. Crear una instancia de UserRemoteDataSource no es tan costoso, de modo que no es necesario establecer su alcance en relación con el ciclo de vida del componente.

@Singleton es la única anotación de alcance que se incluye con el paquete javax.inject. Puedes usarla para anotar ApplicationComponent y los objetos que deseas reutilizar en toda la aplicación.

Kotlin

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

Java

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

Asegúrate de no generar fugas de memoria cuando apliques los alcances a los objetos. Siempre que el componente del alcance esté en la memoria, también se encontrará allí el objeto creado. Debido a que ApplicationComponent se crea cuando se lanza la aplicación (en la clase Application), se destruye cuando se destruye la app. Por lo tanto, la instancia única de UserRepository siempre permanece en la memoria hasta que se destruye la aplicación.

Subcomponentes de Dagger

Si tu flujo de acceso (administrado por una sola LoginActivity) consta de múltiples fragmentos, debes volver a usar la misma instancia de LoginViewModel en todos los fragmentos. @Singleton no puede anotar LoginViewModel para reutilizar la instancia por las siguientes razones:

  1. La instancia de LoginViewModel persistirá en la memoria después de que finalice el flujo.

  2. Necesitas una instancia diferente de LoginViewModel para cada flujo de acceso. Por ejemplo, si el usuario sale, lo mejor es utilizar una instancia diferente de LoginViewModel en lugar de la misma instancia de cuando el usuario accedió por primera vez.

Para establecer el alcance de LoginViewModel en relación con el ciclo de vida de LoginActivity, debes crear un nuevo componente (un nuevo subgrafo) para el flujo de acceso y un nuevo alcance.

Creemos un grafo específico para el flujo de acceso.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Ahora, LoginActivity debería recibir inyecciones de LoginComponent porque tiene una configuración específica para el acceso. Esto quita la responsabilidad de inyectar LoginActivity desde la clase ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent debe poder acceder a los objetos desde ApplicationComponent porque LoginViewModel depende de UserRepository. Para indicarle a Dagger que quieres que un componente nuevo use parte de otro componente, debes emplear subcomponentes de Dagger. El nuevo componente debe ser un subcomponente del componente que contiene los recursos compartidos.

Los subcomponentes son componentes que heredan y extienden el grafo de objetos de un componente superior. Por lo tanto, todos los objetos proporcionados en el componente superior también se proporcionan en el subcomponente. De esta manera, un objeto de un subcomponente puede depender de un objeto proporcionado por el componente superior.

Para crear instancias de subcomponentes, necesitas una instancia del componente superior. Así, los objetos proporcionados por el componente superior al subcomponente conservan el alcance en relación con el componente superior.

En el ejemplo, debes definir LoginComponent como subcomponente de ApplicationComponent. Para hacerlo, anota LoginComponent con @Subcomponent:

Kotlin

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

Java

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

También debes definir una fábrica de subcomponentes dentro de LoginComponent de modo que ApplicationComponent sepa cómo crear instancias de LoginComponent.

Kotlin

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Java

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

Para indicarle a Dagger que LoginComponent es un subcomponente de ApplicationComponent, debes hacer lo siguiente:

  1. Crea un nuevo módulo de Dagger (por ejemplo, SubcomponentsModule) que pase la clase del subcomponente al atributo subcomponents de la anotación.

    Kotlin

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    Java

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. Agrega el nuevo módulo (es decir, SubcomponentsModule) a ApplicationComponent:

    Kotlin

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Java

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    Ten en cuenta que ApplicationComponent ya no necesita inyectar LoginActivity, porque esa responsabilidad ahora le corresponde a LoginComponent, de modo que puedes quitar el método inject() de ApplicationComponent.

    Los usuarios de ApplicationComponent deben saber cómo crear instancias de LoginComponent. El componente superior debe agregar un método a su interfaz para permitir que los usuarios creen instancias del subcomponente a partir de una instancia del componente superior:

  3. Expón la fábrica que crea instancias de LoginComponent en la interfaz:

    Kotlin

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    Java

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

Cómo asignar alcances a los subcomponentes

Si compilas el proyecto, puedes crear instancias de ApplicationComponent y LoginComponent. Se agrega ApplicationComponent al ciclo de vida de la aplicación porque quieres usar la misma instancia del grafo mientras la aplicación está en la memoria.

¿Cuál es el ciclo de vida de LoginComponent? Uno de los motivos por los que necesitabas LoginComponent era que debías compartir la misma instancia de LoginViewModel entre fragmentos relacionados con el acceso. Sin embargo, también te conviene usar instancias diferentes de LoginViewModel cada vez que haya un nuevo flujo de acceso. LoginActivity es el ciclo de vida adecuado para LoginComponent: por cada actividad nueva, necesitas una instancia nueva de LoginComponent y fragmentos que puedan usar esa instancia de LoginComponent.

Debido a que LoginComponent se adjuntó al ciclo de vida de LoginActivity, debes mantener una referencia al componente en la actividad, de la misma manera que mantienes la referencia a applicationComponent en la clase Application. De esta manera, los fragmentos podrán acceder a él.

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

Ten en cuenta que la variable loginComponent no está anotada con @Inject porque no esperas que Dagger proporcione esa variable.

Puedes usar el ApplicationComponent para obtener una referencia a LoginComponent y, luego, inyectar LoginActivity de la siguiente manera:

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Creation of the login graph using the application graph
        loginComponent = (applicationContext as MyDaggerApplication)
                            .appComponent.loginComponent().create()

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

Se crea LoginComponent en el método onCreate() de la actividad y se destruye de forma implícita cuando finaliza la actividad.

El LoginComponent debe proporcionar la misma instancia de LoginViewModel cada vez que se le solicita. Para asegurarte de que lo haga, crea un alcance de anotación personalizado y anota tanto LoginComponent como LoginViewModel. Ten en cuenta que no puedes usar la anotación @Singleton porque ya la utilizó el componente superior y eso convertiría al objeto en un singleton de aplicación (instancia única para toda la app). Debes crear un alcance de anotación diferente.

En este caso, el alcance se podría llamar @LoginScope, pero no se considera una buena práctica. El nombre de la anotación del alcance no debe asociarse explícitamente con el propósito que cumple. En cambio, se le debe asignar un nombre según su ciclo de vida, ya que componentes secundarios como RegistrationComponent y SettingsComponent pueden reutilizar las anotaciones. Por este motivo, debes usar la denominación @ActivityScope en lugar de @LoginScope.

Kotlin

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Ahora, si tienes dos fragmentos que necesitan LoginViewModel, ambos se proporcionan con la misma instancia. Por ejemplo, si tienes un LoginUsernameFragment y un LoginPasswordFragment, deben recibir una inyección de LoginComponent:

Kotlin

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

Java

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

Los componentes acceden a la instancia del componente que reside en el objeto LoginActivity. El código de ejemplo de LoginUserNameFragment aparece en el siguiente fragmento de código:

Kotlin

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Y lo mismo sucede con LoginPasswordFragment:

Kotlin

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

En la figura 3, se muestra cómo se ve el grafo de Dagger con el nuevo subcomponente. Las clases con un punto blanco (UserRepository, LoginRetrofitService y LoginViewModel) son las que tienen un alcance único en relación con sus componentes correspondientes.

Grafo de la aplicación después de agregar el último subcomponente

Figura 3: Representación del grafo que compilaste para el ejemplo de la app para Android

Desglosemos las partes del grafo:

  1. Se incluye el NetworkModule (y, por lo tanto, LoginRetrofitService) en ApplicationComponent porque lo especificaste en el componente.

  2. UserRepository permanece en ApplicationComponent porque el alcance es ApplicationComponent. Si se expande el proyecto, te conviene compartir la misma instancia en diferentes funciones (p. ej., Registro).

    Como UserRepository es parte de ApplicationComponent, sus dependencias (es decir, UserLocalDataSource y UserRemoteDataSource) también deben estar en este componente para que pueda proporcionar instancias de UserRepository.

  3. Se incluye LoginViewModel en LoginComponent porque solo es obligatorio para las clases inyectadas por LoginComponent. No se incluye LoginViewModel en ApplicationComponent porque ninguna dependencia incluida en ApplicationComponent necesita un LoginViewModel.

    Asimismo, si no hubieras definido el alcance de UserRepository para ApplicationComponent, Dagger habría incluido automáticamente UserRepository y sus dependencias como parte de LoginComponent, porque ese es el único lugar en el que se usa UserRepository.

Además de definir el alcance de los objetos en relación con un ciclo de vida diferente, es recomendable crear subcomponentes para encapsular diferentes partes de la aplicación manteniendo la separación entre ellas.

Estructurar la app para crear diferentes subgrafos de Dagger según el flujo de la app ayuda a lograr una aplicación más escalable y de mejor rendimiento en cuanto a memoria y tiempo de inicio.

Prácticas recomendadas para compilar un grafo de Dagger

Cuando compiles el grafo de Dagger para tu aplicación, ten en cuenta lo siguiente:

  • Cuando creas un componente, debes considerar qué elemento es responsable de la vida útil de ese componente. En este caso, la clase Application está a cargo de ApplicationComponent, y LoginActivity está a cargo de LoginComponent.

  • Emplea alcances solo cuando sea necesario. El uso excesivo de alcances puede tener un efecto negativo en el rendimiento del tiempo de ejecución de tu app: el objeto estará en la memoria mientras el componente esté en la memoria, y tener un objeto con alcance es más costoso. Cuando Dagger proporciona el objeto, usa el bloqueo de DoubleCheck, en lugar de un proveedor de tipo de fábrica.

Cómo probar un proyecto que usa Dagger

Uno de los beneficios de usar frameworks de inyección de dependencias, como Dagger, es que facilita la prueba del código.

Pruebas de unidades

No es necesario usar Dagger para las pruebas de unidades. Cuando se prueba una clase que usa la inyección de constructor, no necesitas usar Dagger para crear una instancia de esa clase. Puedes llamar a su constructor pasando dependencias falsas o simuladas directamente, tal como lo harías si no estuvieran anotadas.

Por ejemplo, cuando pruebes LoginViewModel:

Kotlin

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

Java

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

Pruebas de extremo a extremo

Para las pruebas de integración, se recomienda crear un TestApplicationComponent para pruebas. La producción y las pruebas usan configuraciones de componentes distintas.

Así, se requiere un diseño de módulos más directo en la aplicación. El componente de prueba extiende el componente de producción e instala un conjunto diferente de módulos.

Kotlin

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

Java

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModule tiene una implementación falsa del NetworkModule original. Allí puedes proporcionar instancias falsas o simulaciones de lo que quieras reemplazar.

Kotlin

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

Java

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

En las pruebas de integración o de extremo a extremo, debes usar una TestApplication que cree el TestApplicationComponent, en lugar de un ApplicationComponent.

Kotlin

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Java

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

Luego, se usa la aplicación de prueba en un TestRunner personalizado que emplearás para ejecutar pruebas de instrumentación. Si deseas obtener más información sobre este tema, consulta el codelab Cómo usar Dagger en tu app para Android.

Cómo trabajar con módulos de Dagger

Los módulos de Dagger son una forma de encapsular los procedimientos a fin de proporcionar objetos de manera semántica. Puedes agregar módulos en los componentes, pero también puedes incluirlos dentro de otros módulos. Si bien este es un procedimiento muy eficaz, es fácil caer en un uso inadecuado.

Una vez que se agrega un módulo a un componente o a otro módulo, se incorpora en el grafo de Dagger. Dagger puede proporcionar esos objetos al componente. Antes de agregar un módulo, comprueba si ya forma parte del grafo de Dagger. Para ello, verifica si ya se agregó al componente, o bien compila el proyecto y observa si Dagger puede encontrar las dependencias requeridas para ese módulo.

Se recomienda que se declaren los módulos solo una vez por un componente (fuera de los casos prácticos avanzados específicos de Dagger).

Supongamos que tienes tu grafo configurado de esta manera. ApplicationComponent incluye Module1 y Module2, mientras que Module1 incluye ModuleX.

Kotlin

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

Ahora, Module2 depende de las clases proporcionadas por ModuleX. No se recomienda incluir ModuleX en Module2, porque, de esta manera, ModuleX aparece dos veces en el grafo, como se muestra en el siguiente fragmento de código:

Kotlin

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

Java

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

En cambio, deberías realizar una de las siguientes acciones:

  1. Refactoriza los módulos y extrae el módulo común del componente.
  2. Crea un módulo nuevo con los objetos que comparten ambos módulos y extráelo del componente.

Si no se refactoriza el código de esta manera, se genera una gran cantidad de módulos incluidos unos dentro de otros sin un sentido claro de organización y que hacen que sea más difícil ver de dónde proviene cada dependencia.

Recomendación (opción 1): Se declara ModuleX una vez en el grafo de Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

Recomendación (opción 2): Se extraen las dependencias comunes de Module1 y Module2 de ModuleX y se incorporan a un nuevo módulo, llamado ModuleXCommon, que se incluye en el componente. Luego, se crean otros dos módulos, llamados ModuleXWithModule1Dependencies y ModuleXWithModule2Dependencies, con las dependencias específicas de cada módulo. Se declaran todos los módulos una vez en el grafo de Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

Inserción asistida

La inserción asistida es un patrón de DI que se usa para construir un objeto en el que el framework de DI puede proporcionar algunos parámetros y el usuario debe pasar otros en el momento de la creación.

En Android, este patrón es común en las pantallas de detalles, donde el ID del elemento que se muestra solo se conoce en el tiempo de ejecución, no en el tiempo de compilación, cuando Dagger genera el grafo de DI. Para obtener más información sobre la inserción asistida con Dagger, consulta la documentación de Dagger.

Conclusión

Si aún no lo hiciste, consulta la sección de prácticas recomendadas. Para ver cómo usar Dagger en una app para Android, consulta el codelab Cómo usar Dagger en tu app para Android.