The Android Developer Challenge is back! Submit your idea before December 2.

Using Dagger in multi-module apps

In a project with multiple Gradle modules (known as a multi-module project) that ships as a single APK with no dynamic feature modules, it's common to have an app module containing all shared logic across the application. The app module is a good place to declare your application component with all the shared logic. In the app module, you can provide objects such as OkHttpClient objects, JSON parsers, accessors for your database, and SharedPreference objects.

In the app module, you could also have other components with shorter lifespans. An example could be a UserComponent with user-specific configuration (like a UserSession) after a log in.

In the different modules of your project, you can define at least one subcomponent that has logic specific to that module as seen in figure 1.

Figure 1. Example of a Dagger graph in a multi-module project

For example, in a login module, you could have a LoginComponent scoped with a custom @ModuleScope annotation that can provide objects common to that feature such as a LoginRepository. Inside that module, you can also have other components that depend on a LoginComponent with a different custom scope, for example @FeatureScope for a LoginActivityComponent or a TermsAndConditionsComponent where you can scope more feature-specific logic such as ViewModel objects.

For other modules such as Registration, you would have a similar setup.

A general rule for a multi-module project is that modules of the same level shouldn't depend on each other. If they do, consider whether that shared logic (the dependencies between them) should be part of the parent module. If so, refactor to move the classes to the parent module; if not, create a new module that extends the parent module and have both of the original modules extend the new module.

When you use this refactoring strategy, you could have many levels of modules in your app. A good practice is exposing at least one component per module by making it public so that other modules can use it.

Component dependencies with dynamic feature modules

With dynamic feature modules, the way modules usually depend on each other is inverted. Instead of the app module including feature modules, the dynamic feature modules depend on the app module. See figure 2 for a representation of how modules are structured.

Figure 2. Example of a Dagger graph in a project with dynamic feature modules

In Dagger, components need to know about their subcomponents. This information is included in a Dagger module added to the parent component (like the SubcomponentsModule module in Using Dagger in Android apps).

Unfortunately, with the reversed dependency between the app and the dynamic feature module, the subcomponent is not visible from the app module because it's not in the build path. As an example, a LoginComponent defined in a login dynamic feature module cannot be a subcomponent of the ApplicationComponent defined in the app module.

Dagger has a mechanism called component dependencies that you can use to solve this issue. Instead of the child component being a subcomponent of the parent component, the child component is dependent on the parent component. With that, there is no parent-child relationship; now components depend on others to get certain dependencies. Components need to expose types from the graph for dependent components to consume them.

For example: a dynamic feature module called login wants to build a LoginComponent that depends on the AppComponent available in the app Gradle module.

Below are definitions for the classes and the AppComponent that are part of the app Gradle module:

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

In your login gradle module that includes the app gradle module, you have a LoginActivity that needs a LoginViewModel instance to be injected:

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 has a dependency on UserRepository that is available and scoped to AppComponent. Let's create a LoginComponent that depends on AppComponent to inject 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 specifies a dependency on AppComponent by adding it to the dependencies parameter of the component annotation. Because LoginActivity will be injected by Dagger, add the inject() method to the interface.

When creating a LoginComponent, an instance of AppComponent needs to be passed in. Use the component factory to do it:

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

Now, LoginActivity can create an instance of LoginComponent and call the inject() method.

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 depends on UserRepository; and for LoginComponent to be able to access it from AppComponent, AppComponent needs to expose it in its interface:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userManager(): UserManager
}

Java

@Singleton
@Component
public interface AppComponent {
    UserManager userManager();
}

The scoping rules with dependent components work in the same way as with subcomponents. Because LoginComponent uses an instance of AppComponent, they cannot use the same scope annotation.

If you wanted to scope LoginViewModel to LoginComponent, you would do it as you did previously using the custom @ActivityScope annotation.

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;
    }
}

Best practices

  • For Gradle modules that need to build a graph, include a public Dagger component that other Gradle modules can either consume or extend as needed. If the same Gradle module needs to have other Dagger components, make them package-private so that they're not visible to other Gradle modules.

  • For Gradle modules that are meant to be utilities or helpers and don't need to build a graph, make Dagger modules public so that they can be used in other Gradle modules.

  • In an app with multiple Gradle modules, use component dependencies instead of subcomponents when the components are in different Gradle modules.