An Easier Alternative to Subcomponents

Subcomponents is very hard to grasp especially when you’re first starting out with Dagger. In the Advanced Dagger Tutorial, we were using Subcomponents to provide dependencies to our activities. In the next sections, we’re going to learn an alternative way to Subcomponents.

Contents

  1. Why Dagger Subcomponents are hard to understand
  2. Quick tour of the project
  3. Rules for using component dependencies
  4. Refactor and use component dependencies
  5. When to use Subcomponents and Component Dependencies
  6. Final thoughts
  7. Source Code (check component-dependencies branch)

Why Dagger Subcomponents are hard to understand

Subcomponents are hard to understand when you’re first starting out with Dagger because of how they’re kinda “coupled” together using a parent-child relationship. Let’s take this for example:

@Singleton
@Component(
    modules = [
        AppModule::class
    ]
)
interface AppComponent {

    fun userDetailsSubcomponent(): UserDetailsSubcomponent.Builder

    fun reposSubcomponent(): ReposSubcomponent.Builder
}

In the AppModule class is where we declare our Subcomponents.

@Module(
    subcomponents = [
        UserDetailsSubcomponent::class,
        ReposSubcomponent::class
    ]
)
class AppModule { ... }

One of the confusing part is why Subcomponents are declared in the parent component’s module and not in the parent component directly. Maybe something like:

@Singleton
@Component(
    modules = [
        AppModule::class
    ],
    subcomponents = [
        UserDetailsSubcomponent::class,
        ReposSubcomponent::class
    ]
)
interface AppComponent { ... }

That could’ve been less confusing. Maybe there are some underlying issues that we don’t know?

In th next section, we’ll start refactoring a project that uses Subcomponents to Component Dependencies - an alternative way of Subcomponents. Let’s get started.

Quick tour of the project

Import the project

  1. Download or clone the project here
  2. Open the project in your Android Studio
  3. Run the app
  4. Enter a Github username to test

The current implementation

Let’s first examine our current implementation which uses the Subcomponent approach.

Our AppModule under di package is where the subcomponents are declared.

@Module(
    subcomponents = [
        UserDetailsSubcomponent::class,
        ReposSubcomponent::class
    ]
)
class AppModule { ... }

Our AppComponent under di package has methods that return our subcomponents’ builders.

@Singleton
@Component(
    modules = [
        AppModule::class
    ]
)
interface AppComponent {

    fun userDetailsSubcomponent(): UserDetailsSubcomponent.Builder

    fun reposSubcomponent(): ReposSubcomponent.Builder
}

Our ReposActivity and UserDetailsActivity is where we use the methods in our AppComponent to instantiate their respective subcomponents.

This is how we instantiated a UserDetailsSubcomponent in UserDetailsActivity:

class UserDetailsActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val component = appComponent
                .userDetailsSubcomponent()
                .build()
        component.inject(this)

        ...
    }
}

Similary in ReposActivity, we instantiated a ReposSubcomponent:

class ReposActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val component = appComponent
            .reposSubcomponent()
            .build()
        component.inject(this)

        ...
    }
}

If you want to know more about Subcomponents, check out our Advanced Dagger Tutorial.

Before we use Component Dependencies, there are a few rules that you need to keep in mind.

Rules for using component dependencies

  1. Your component must have a different @Scope from the component that you depend on.
  2. Your component must have a @Scope if the component that you depend on has a @Scope.
  3. Unlike Subcomponents where you can access everything in your parent component, when using component dependencies, you can longer access everything because there’s no more parent-child relationship. The component that you depend on must expose the dependencies that you need.

We’ll go over these rules in detail in the next section where we start refactoring the code.

Refactor and use Component Dependencies

Cleaning things up first

Remove the subcomponent instantiation in UserDetailsActivity.


class UserDetailsActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val component = appComponent
              .userDetailsSubcomponent()
              .build()
        component.inject(this)

        ...
    }
}

Do the same in ReposActivity.


class ReposActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val component = appComponent
            .reposSubcomponent()
            .build()
        component.inject(this)

        ...
    }
}

Open AppComponent and remove the methods that were used to instantiate our subcomponents.


@Singleton
@Component(
    modules = [
        AppModule::class
    ]
)
interface AppComponent {

    fun userDetailsSubcomponent(): UserDetailsSubcomponent.Builder

    fun reposSubcomponent(): ReposSubcomponent.Builder
}

Open AppModule and remove the subcomponent declarations and what should be left is the @Module annotation.


@Module(
    subcomponents = [
        UserDetailsSubcomponent::class,
        ReposSubcomponent::class
    ]
)
class AppModule { ... }

Creating our components

Unlike subcomponents where we use the @Subcomponent annotation. In a component dependencies approach, we just use our normal @Component annotation similar to our AppComponent.

Under userdetails package, create a new interface called UserDetailsComponent.

@Component(
    modules = [UserDetailsModule::class]
)
interface UserDetailsComponent {

    fun inject(activity: UserDetailsActivity)
}

As you can see, it pretty much looks like a normal component like AppComponent. To use component dependencies, we will use the dependencies parameter found in the @Component annotation.


@Component(
    modules = [UserDetailsModule::class],
    dependencies = [AppComponent::class]
)
interface UserDetailsComponent {

    fun inject(activity: UserDetailsActivity)
}

This means that our UserDetailsComponent is telling Dagger - “Hey Dagger, I have some dependencies that I need for my own module(s) but only AppComponent provides them so I’m going to list it as one my dependencies.” If you take a look at UserDetailsModule, you’ll see that we need an Api and AppComponent is the one responsible for providing it.

Remember rule #1 & #2?

#1: Your component must have a different @Scope from the component that you depend on.

#2: Your component must have a @Scope if the component that you depend on has a @Scope.

Since AppComponent already uses the @Singleton scope, we’re going to annotate our component with @ActivityScope found in di package and since AppComponent has a scope our component needs a scope as well because Dagger doesn’t allow you to depend on scoped components without you having a scope of your own.

If you’d like to learn more about scoping, our Basic Dagger Tutorial covers that.

Here’s the complete code for UserDetailsComponent:

@ActivityScope
@Component(
    modules = [UserDetailsModule::class],
    dependencies = [AppComponent::class]
)
interface UserDetailsComponent {

    fun inject(activity: UserDetailsActivity)
}

Let’s the do same for our repos package. Create a new interface called ReposComponent.

@ActivityScope
@Component(
    modules = [ReposModule::class],
    dependencies = [AppComponent::class]
)
interface ReposComponent {

    fun inject(activity: ReposActivity)
}

If try to build the app, Build -> Make Project, you’ll see that it errors which says:

Api cannot be provided without an @Provides-annotated method.

Remember, rule #3 states that:

Unlike Subcomponents where you can access everything in your parent component, when using component dependencies, you can longer access everything because there’s no more parent-child relationship. The component that you depend on must expose the dependencies that you need.

Now that we’ve severed that parent-child relationship. Our Appcomponent, which was supposed to be the parent, is now independent and standalone (I didn’t intend it to sound this harsh). AppComponent has no longer any idea if there are other Dagger Components that exist as well. So how are going to expose a dependency?

It’s very simple. Open your AppComponent and create a new method that returns an Api.


@Singleton
@Component(
    modules = [
        AppModule::class
    ]
)
interface AppComponent {

    fun api(): Api
}

The method name doesn’t matter. You can name the method whatever you want because the important thing is the return type of that method.

AppComponent is telling Dagger that - “Hey Dagger, I don’t know if there are other components that depend on me but I’m exposing an Api dependency that I’m responsible for creating just in case they need it.”

That’s the key difference between Subcomponents and Component Dependencies. So if ever you need something from the component that you depend on, make sure to make a method in that component which returns the dependency that you need.

Instantiating our Activities’ respective components

Before you do this, make sure to build the app - Build -> Make Project.

Open your UserDetailsActivity and instantiate a UserDetailsComponent.


class UserDetailsActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_details)
        
        val component = DaggerUserDetailsComponent
            .builder()
            .appComponent(appComponent)
            .build()
        component.inject(this)

        ...
    }
}

Similar to AppComponent where Dagger generates a class prefixed with the component name - DaggerAppComponent. It does the same for our components as well because they’re just normal components.

One key thing that you see is how we passed an appComponent to the appComponent() method of the builder. Now that you depend on a component, Dagger will generate a method that asks you to supply the component(s) that you depend on or else you can’t access the exposed dependency. The method generated is similar to the name of the component that you depend on but lowercase as first letter.

Let’s do the same for our ReposActivity.


class ReposActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_repos)
        
        val component = DaggerReposComponent
            .builder()
            .appComponent(appComponent)
            .build()
        component.inject(this)

        ...
    }
}

Run the app and check if it works the same as before.

When to use Subcomponents and Component Dependencies

It depends. For example, Component Dependencies are pretty common in module by feature projects. They have like a core module that contains the common classes that all feature modules need and each feature module depends on a CoreComponent of some sort. However, each feature module uses Subcomponents inside of them to wire things up.

If you think that you only need just a couple of dependencies, maybe component dependencies might be enough. If you think that you need a lot, using subcomponents might work for you. It really depends on your use case and it will be up to you to decide as Android developer.

Final Thoughts

If you’re just starting out with scoping and separating components, don’t worry about when to use and when not to use the two approaches. Use the one that doesn’t confuse you the most. Practice and practice and after you get comfortable and understand the flow, try experimenting the other method. Take your time to learn and eventually you’ll have the knowledge when to use them and when not to.

If you want to learn basic scoping, check out our Basic Dagger Tutorial.

If you want to learn advanced scoping and subcomponents, check out our Advanced Dagger Tutorial.

If you want to be notified for more Android tutorials, you can subscribe to my newsletter below and get a free book on the 7 ways to become a really good Android developer!👇

7 ways to become a really good Android developer