Cómo usar Dagger en apps de varios módulos

Gradle ofrece la posibilidad de crear proyectos de varios módulos. En un proyecto de varios módulos que se envía como un solo APK sin módulos de funciones, es común tener un módulo app que puede depender de la mayoría de los módulos del proyecto y un módulo base o core del que suelen depender los demás módulos. En general, el módulo app contiene la clase Application, mientras que el módulo base contiene todas las clases comunes que se comparten entre los distintos módulos de tu proyecto.

El módulo app es un buen lugar para declarar el componente de la aplicación (por ejemplo, ApplicationComponent en la imagen a continuación) que puede proporcionar los objetos que otros componentes podrían necesitar, además de los singletons de tu app. Por ejemplo, el ApplicationComponent definido en el módulo app proporcionará clases como OkHttpClient, analizadores de JSON, descriptores de acceso para la base de datos y objetos SharedPreferences que se pueden definir en el módulo core.

En el módulo app, también puedes tener otros componentes con una vida útil más corta. Un ejemplo podría ser un UserComponent con una configuración específica de usuario (como una UserSession) después de un acceso.

En los diferentes módulos del proyecto, puedes definir al menos un subcomponente que tenga una lógica específica para ese módulo, como se muestra en la figura 1.

Figura 1: Ejemplo de un grafo de Dagger en un proyecto de varios módulos

Por ejemplo, en un módulo login, podrías tener un LoginComponent con alcance establecido mediante una anotación @ModuleScope personalizada capaz de proporcionar objetos comunes a esa característica, como un LoginRepository. Dentro de ese módulo, también puedes tener otros componentes que dependan de un LoginComponent con un alcance personalizado diferente, por ejemplo, @FeatureScope para un LoginActivityComponent o un TermsAndConditionsComponent donde puedes establecer el alcance de lógicas más específicas según las funciones, como los objetos ViewModel.

Para otros módulos como Registration, tendrías una configuración similar.

Como regla general, en un proyecto de varios módulos, los módulos del mismo nivel no deberían depender unos de otros. De ser así, considera si la lógica compartida (las dependencias mutuas) debería formar parte del módulo superior. En ese caso, refactoriza el proyecto para mover las clases al módulo superior; en caso contrario, crea un módulo nuevo que extienda el módulo superior y haz que ambos módulos originales extiendan el módulo nuevo.

Como práctica recomendada, generalmente deberías crear un componente en un módulo en los siguientes casos:

  • Cuando necesitas realizar una inserción de campo, como con LoginActivityComponent.

  • Cuando necesitas establecer el alcance de objetos, como con LoginComponent.

Aparte de estos casos, si necesitas indicarle a Dagger cómo proporcionar objetos desde ese módulo, crea y expón un módulo de Dagger con los métodos @Provides o @Binds si la inserción de construcción no es posible para esas clases.

Implementación con subcomponentes de Dagger

La página de documentación Cómo usar Dagger en apps para Android incluye información sobre cómo crear y usar subcomponentes. Sin embargo, no puedes usar el mismo código porque los módulos de funciones no conocen el módulo app. A modo de ejemplo, toma un flujo de Login típico y el código de la página anterior, y verás que la compilación ya no puede completarse:

Kotlin

class LoginActivity: Activity() {
  ...

  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)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @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);

        ...
    }
}

Eso se debe a que el módulo login no conoce MyApplication ni appComponent. Para que funcione, deberías definir una interfaz en el módulo de funciones que proporcione un FeatureComponent que MyApplication necesite implementar.

En el siguiente ejemplo, puedes definir una interfaz LoginComponentProvider que proporcione un LoginComponent en el módulo login para el flujo de Login:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Ahora, LoginActivity usará esa interfaz en lugar del fragmento de código definido previamente:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

Ahora, MyApplication necesita implementar esa interfaz y los métodos requeridos:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

Así es como puedes usar subcomponentes de Dagger en un proyecto de varios módulos. Con los módulos de funciones, la solución es diferente por la forma en que los módulos dependen unos de otros.

Dependencias de componentes con módulos de funciones

Con los módulos de funciones, se invierte la forma en que los módulos suelen depender entre sí. En lugar de que el módulo app incluya módulos de funciones, los módulos de funciones dependen del módulo app. Consulta una representación de cómo se estructuran los módulos en la figura 2.

Figura 2: Ejemplo de un grafo de Dagger en un proyecto con módulos de funciones

En Dagger, los componentes deben conocer sus subcomponentes. Esta información se incluye en un módulo de Dagger que se agrega al componente superior (como el módulo SubcomponentsModule en Cómo usar Dagger en apps para Android).

Lamentablemente, con la dependencia invertida entre la app y el módulo de funciones, el subcomponente no es visible desde el módulo app porque no está en la ruta de compilación. Como ejemplo, un LoginComponent definido en un módulo de funciones login no puede ser un subcomponente del ApplicationComponent definido en el módulo app.

Dagger tiene un mecanismo llamado dependencias de componentes que puedes usar para resolver este problema. En lugar de ser un subcomponente del componente superior, el componente secundario depende de aquel. Así, no existe relación componente superior/componente secundario. Ahora, los componentes dependen de otros para obtener algunas dependencias. Los componentes deben exponer tipos del grafo para que los componentes dependientes los consuman.

Por ejemplo, un módulo de funciones llamado login quiere crear un LoginComponent que dependa del AppComponent disponible en el módulo app de Gradle.

A continuación se muestran las definiciones de las clases y el AppComponent que forman parte del módulo app de Gradle:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@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;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

En tu módulo login de Gradle que incluye el módulo app de Gradle, tienes una LoginActivity que necesita la inserción de una instancia LoginViewModel:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

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

LoginViewModel tiene una dependencia en UserRepository que está disponible en AppComponent y cuyo alcance se estableció con relación a ese componente. Creemos un LoginComponent que dependa de AppComponent para insertar una LoginActivity:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent especifica una dependencia en AppComponent agregándola al parámetro de dependencias de la anotación del componente. Como Dagger insertará LoginActivity, agrega el método inject() a la interfaz.

Cuando se crea un LoginComponent, se debe pasar una instancia de AppComponent. Usa la fábrica de componentes para hacerlo:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

Ahora, LoginActivity puede crear una instancia de LoginComponent y llamar al método inject().

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel depende de UserRepository; y para que LoginComponent pueda acceder a él desde AppComponent, AppComponent debe exponerlo en su interfaz:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

Las reglas de establecimiento de alcance con componentes dependientes funcionan igual que con los subcomponentes. Como LoginComponent usa una instancia de AppComponent, no puede usar la misma anotación de alcance.

Si deseas establecer el alcance de LoginViewModel con relación a LoginComponent, debes hacerlo igual que antes con la anotación @ActivityScope personalizada.

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

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

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

Prácticas recomendadas

  • ApplicationComponent siempre debe estar en el módulo app.

  • Crea componentes de Dagger en módulos si necesitas realizar una inserción de campo en ese módulo o si necesitas establecer el alcance de objetos para un flujo específico de la aplicación.

  • En los módulos de Gradle creados como utilidades o asistentes, y que no necesiten compilar un grafo (el motivo por el que se requeriría un componente de Dagger), crea y expón módulos de Dagger públicos con los métodos @Provides y @Binds de las clases que no admiten la inserción de constructores.

  • Si quieres usar Dagger en una app para Android con módulos de funciones, usa las dependencias de componentes a fin de acceder a dependencias provistas por el ApplicationComponent definido en el módulo app.