For the longest time, I just made do with a single AppComponent that I kept in my Application subclass. Whenever I needed to inject dependencies, I’d just call getApplication(), cast it and grab my component. Part of it was because I didn’t understood scopes and subcomponents. The other part was because the projects I worked on weren’t big enough to require another component.

A single AppComponent is good enough for smaller projects, but as your application grows in complexity, it starts becoming hard to manage using 1 component and a couple scopes.

tl;dr

Scopes tell Dagger when to make a new instance when injecting dependencies. Subcomponents are good if you want full-access to the parent component. Component dependencies are good if you want to control what is accessible between components. This is useful for making sure states are persisted only within the same context. Like if you had two instances of the same Activity, you don’t want them to share the same presenter. So, you scope the presenter to the activity, you make each Activity get a different presenter instance. You scope classes using a custom annotation and defining a new component that uses the same annotation.

TBH

The tl;dr doesn’t come close to an adequate explanation, but without the nitty gritty, that’s the best I can do. I mean, we’re talking about a library where the official documentation says things like

In other words, the object graph of a subcomponent’s parent component is a subgraph of the object graph of the subcomponent itself.

I had to read that like 10 times before I understood it.

Some Exposition on Scopes and Subcomponents

So, aside from your common Dagger terminology (dependency graph, injection, component, module, etc.), there’s two more terms you need to know:

  • Scope
  • Subcomponent

Scope refers to the group of classes that a component applies to. This is marked by an included annotation, like @Singleton or a custom annotation like @ActivityScope. You can have dependencies scoped to Activities or dependencies scoped to Fragments. Scoping is used to determine how instances are shared and generated.

Subcomponent refers to a component that relies on a parent component. It’s kind of like a subclass. It’s marked with @Subcomponent. Components can depend on zero or more other components, but it works differently from subcomponents.

So, here’s a question. Why use subcomponents if regular components can depend on other components? It’s nuanced, but can be explained like this:

  • Subcomponents have access to the entire graph of the parent component. That is, they can access all the dependencies of the parent. It’s tightly coupled.
  • Component Dependencies only have access to what the other component exposes.
    • So, say ComponentChocolate depends on ComponentCandy.
    • ComponentCandy has two dependencies, Lollipop and Oreo.
    • Oreo is exposed in ComponentCandy and Lollipop is not.
    • Therefore, ComponentChocolate can only access Oreo.

As a rule of thumb, I would start with subcomponents. Use component dependencies when you have a lot of modules and need to share dependencies across all of them.

Quickly, Unscoped Means…?

There’s also the concept of being unscoped. For provided dependencies, this means Dagger will re-evaluate the @Provides method whenever the dependency is needed. Typically, this generates a new instance every time.

All components and dependencies are unscoped unless you explicitly say otherwise. You can specify otherwise by using a @Scope annotation.

About the Android Injector

One more thing, I will not be using the Android Injector classes introduced in Dagger 2.11. That itself is complex enough to warrant its own article.

Singleton Scope - Application Scope

Let’s define our basic AppComponent, AppModule and a basic network dependency.

    class NetworkClient {
        // HTTP goodness
    }

    @Module
    class AppModule() {
        // this is an unscoped dependency
        @Provides
        fun providesNetworkClient(): NetworkClient {
            return NetworkClient()
        }
    }

    // this is an unscoped component
    // if we scoped the dependency with @Singleton
    // then Dagger would get a compiler error
    @Component(modules = [AppModule::class])
    interface AppComponent {
        fun inject(activity: MainActivity)
    }

Alright, that’s a basic Dagger setup. Let’s then instantiate it in our MyApplication class

    class MyApplication : Application() {
        // activities won't call for the appComponent until
        // Application.onCreate is called
        // so we can lazily instantiate the appComponent instance
        val appComponent by lazy {
            DaggerAppComponent.builder().build()
        }
    }

And inject it into our MainActivity

    class MainActivity : AppCompatActivity() {
        @Inject private lateinit var client: NetworkClient

        override fun onCreate(savedInstanceState: Bundle?) {
            // grab the app component to inject dependencies
            (application as? MyApplication)?.appComponent?.inject(this)

            // run the rest of the setup afterwards
            // if we have dagger fragments, we'd want our dependencies to be ready
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    }

This all was pretty normal in the early days of Dagger 2. Since the dependency and component are unscoped, the instance of NetworkClient we get is a new one, every time.

If you annotate AppComponent and AppModule.provideNetworkClient with @Singleton, we can make the dependency a “local singleton”. Let’s do that

    @Module
    class AppModule() {
        @Singleton
        @Provides
        fun providesNetworkClient(): NetworkClient {
            return NetworkClient()
        }
    }

    @Singleton
    @Component(modules = [AppModule::class])
    interface AppComponent {
        fun inject(activity: MainActivity)
    }

This means for this instance of AppComponent the same instance of NetworkClient will be returned. If we made a new instance of AppComponent, we would have a new instance of NetworkClient.

Extending from the Application Scope - Activity Scope

Now, let’s start getting a little more fancy. I mentioned using custom annotations to define scopes before. There’s nothing special about these custom annotations. They are used as identifiers and provide no inherent functionality.

    // dagger annotation to specify this annotation class is a scope
    @Scope
    // annotation class that determines when this class should be removed
    // by default, it's set for RUNTIME which means it won't ever be removed
    @Retention
    // the name of the class is entirely arbitrary
    annotation class ActivityScope

Once that’s defined, we can construct our first subcomponent

    @ActivityScope
    @Subcomponent
    interface ActivityComponent {
        fun inject(activity: MainActivity)
    }

So, we have an ActivityComponent, but what is it doing? Nothing that AppComponent isn’t already doing. The usefulness of a subcomponent becomes clear when you have dependencies specific to the scope. So, let’s define a new module that provides Activity-specific dependencies.

    class ActivityLogger {
        fun log(s: String) {
            println(s)
        }
    }

    @Module
    class ActivityModule {
        @ActivityScope
        @Provides
        fun provideActivityLogger(): ActivityLogger {
            return ActivityLogger()
        }
    }

Then, tie it to our ActivityComponent

    @ActivityScope
    @Subcomponent(modules = [ActivityModule::class])
    interface ActivityComponent {
        // ...
    }

As you’ve noticed, we haven’t referenced the AppComponent at all. So, how does it know what to extend?

It doesn’t.

We need to update our AppComponent

    @Singleton
    @Component(modules = [AppModule::class])
    interface AppComponent {
        // remember to remove this call
        // it will conflict with the ActivityComponent inject
        // fun inject(activity: MainActivity)

        // the syntax for exposing the subcomponent is a little convoluted
        // we need to specify the component type
        // and its dependencies
        fun activityComponent(module: ActivityModule): ActivityComponent
    }

After that, we need to integrate the component to the Activity’s lifecycle. We have the following options for that:

  • re-instantiate ActivityComponent every time in onCreate
  • keep the activity component in the MyApplication like AppComponent
  • persist the ActivityComponent instance using onRetainCustomConfigurationInstance and on

Personally, I find the last option to be the best. It allows us persist the component across config changes. This, in effect, persists our scoped dependencies, as well. So, if you scope a Presenter to the activity, then by persisting the component, you persist the Presenter instance.

    class MainActivity : AppCompatActivity() {

        @Inject lateinit var networkClient: NetworkClient
        @Inject lateinit var activityLogger: ActivityLogger

        lateinit var activityComponent: ActivityComponent

        override fun onCreate(savedInstanceState: Bundle?) {
            activityComponent = lastCustomNonConfigurationInstance as? ActivityComponent
                    ?: createActivityComponent()
            activityComponent.inject(this)

            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }

        private fun createActivityComponent(): ActivityComponent {
            return (application as MyApplication)
                .appComponent
                .activityComponent(ActivityModule())
        }

        override fun onRetainCustomNonConfigurationInstance(): Any {
            return activityComponent
        }
    }

If your component holds a reference to the Activity in any way, you will need to null the component ref in onDestroy.

    // remove the reference when finishing to avoid leaks
    // this is really only necessary if your component holds a ref to the Activity
    override fun onDestroy() {
        if (isFinishing) {
            activityComponent = null
        }
        super.onDestroy()
    }

This approach requires that you change the component type to ActivityComponent?, which will require safety operators.

Extending from the Activity Scope - Fragment Scope

Subcomponents can keep going deeper and deeper. For most intents and purposes, defining an Activity and Fragment component is more than enough.

Start by defining another scope annotation

    @Scope
    @Retention(RetentionPolicy.RUNTIME)
    annotation class FragmentScope

Then, we repeat what we did earlier- create the module and component

    class FragmentLogger {
        fun log(s: String) {
            println(s)
        }
    }

    @Module
    class FragmentModule {

        @FragmentScope
        @Provides
        fun providesFragmentLogger(): FragmentLogger {
            return FragmentLogger()
        }
    }

    @FragmentScope
    @Subcomponent(modules = [FragmentModule::class])
    interface FragmentComponent {
        fun inject(fragment: MainFragment)
    }

And integrate it

    class MainFragment : Fragment() {
        @Inject lateinit var networkClient: NetworkClient
        @Inject lateinit var logger: FragmentLogger

        override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
            return inflater.inflate(R.layout.fragment_main, container, false)
        }

        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            (activity as MainActivity)
                .activityComponent
                .fragmentComponent(FragmentModule())
                .inject(this)

            fragment_text.text = logger.log("").toString()
        }
    }

For Fragments, there’s various ways to handle config changes, which can be used to persist the FragmentComponent.

You could persist it in the parent activity if the fragment is present for the same duration as the activity.

You could persist it in another headless fragment with setRetainInstance(true) or in the same fragment with the same method. If you go with the latter, be sure to clean up references that shouldn’t stay alive.

Protip: Side Stepping @Provides

You don’t need to define a @Provides method to expose a dependency. If you use constructor injection and add a scope annotation on the class declaration, Dagger will know to generate code for that class. It will be scoped appropriately, as well.

    @ActivityScope
    class ScopedDependencyExample
    @Inject
    constructor(private val networkClient: NetworkClient) {
        // ...
    }

Source Code

All of the code is available on GitHub if you wanted to look over it.

See How the Android Injector Makes Things Easier

Alright, since we’ve covered scopes and subcomponents, we can move onto how Dagger makes this easier for Android.

Dagger’s Android Injector Explained

References

android dagger scopes subcomponents