Dagger 2 reference documentation on Subcomponent is compact. It's not easily digestible. So I made one version referring to its content, and add illustration and code in Kotlin. Hope that helps make it easier to consume.

If you want a basic simple illustration on Subcomponent, you could also refer

In Dagger 2, component is the main container-like object that binds all the dependencies (or it's factory).

Subcomponent are components that is like an extension to its parent component.

None

It can be used for

  1. Partition the dependencies into different compartments. Avoid the parent component to have too many dependencies bound to it.
  2. Subcomponent and its parent component have different scope (of lifespan). The subcomponent scope is smaller than its parent.

Subcomponent can access all its parent bound dependencies, but not vice versa. The relationship is illustrated further below.

None

Declaring a subcomponent

To create as subcomponent,

  1. Use @Subcomponent to annotate on an interface (or abstract class).
  2. Add your subcomponent modules by adding to the Module parameter.
  3. Create the Builder using @Subcomponent.Builder. (Optional, but preferred in some cases. Refer to this blog for comparison.
@Subcomponent(modules = [RequestModule::class])
internal interface RequestComponent {
    fun requestHandler(): RequestHandler
    @Subcomponent.Builder
    interface Builder {
        // the requestModule is optional if the Module 
        // doesn't have any constructor argument
        fun requestModule(module: RequestModule): Builder
        // the bindData is optional 
        @BindsIntance
        fun bindData(data: Data): Builder
        fun build(): RequestComponent
    }
}
None

Adding a subcomponent to a parent component

Unlike coding inheritance, where the child define who the parent is, in Dagger 2, the parent define who the child (subcomponent) is.

In another word, the child could have multiple parents, as long each of the parent has all the child needed dependency

There are two approach

1. Through the parent component's module

Add the subcomponent class to the subcomponents attribute of a @Module that the parent component installs.

@Module(subcomponents = [RequestComponent::class])
class ServerModule
@Singleton
@Component(modules = [ServerModule::class])
interface ServerComponent {
    val requestRouter: RequestRouter
}

Note: Even thought the Subcomponent is linked through the ServerModule, other module (if there is) within the ServerModule can also have access to the Subcomponent's builder.

2. Through the parent component abstract function.

Create an abstract function (or in Kotlin, we could have interface property) that generate the Subcomponent's Builder (or the subcomponent itself)

@Singleton
@Component(modules = [ServerModule::class])
interface ServerComponent {
    val requestRouter: RequestRouter
    val requestComponentBuilder: RequestComponent.Builder
}
None

As of which to use or not, refer to this blog for comparison.

With either of the approach above, the parent bounded dependency can be injected with the child component Builder

@Singleton
class RequestRouter @Inject constructor(
  private val requestComponentProvider:     
    Provider<RequestComponent.Builder>) {
    fun dataReceived(data: Data) {
       val requestComponent = requestComponentProvider.get()
            .requestModule(RequestModule()) // Optional
            .data(data) // Required if BindsInstance is defined.
            .build()
       requestComponent.requestHandler.writeResponse(200, "Hello")
    }
}
None

Subcomponents and scope

In Dagger 2, Scope is used to help determined the lifespan of an object. An un-scope object is always created a new when being called. But a Scope object is created once per scope defined lifespan. Refer the below blog to understand more

For Subcomponent, it enables one to have smaller scope than what the parent component has. The default scope is @Singleton. If the parent component @Singleton, then its subcomponent need to have a smaller scope, which can only be constructed using custom scope.

Example custom scopes is written as below

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ChildScope

In term of Scope, the rules as below

  • The parent component and child component (or anything between the ancestry line) cannot be of the same scope
  • Un-scope parent component can refer to a scope child component.
  • The sibling components could may be using the same scope annotation, but they scope lifespan is different (within the respective subcomponent build existence)
@RootScope @Component
interface RootComponent {
  val rootScopeChildComponent: RootScopeComponent.Builder // ERROR!
  val siblingComponentOne: SiblingComponentOne.Builder // OK
  val siblingComponentTwo: SiblingComponentTwo.Builder // OK
}

@RootScope @Subcomponent
interface RootScopeComponent {...}

@ChildScope @Subcomponent
interface SiblingComponentOne {...}

@ChildScope @Subcomponent
interface SiblingComponentTwo {...}
@Component
interface UnscopeComponent {
  val rootScopeChildComponent: RootScopeComponent.Builder // OK
}
None

Another example, we have a RootComponent that is @Singleton scope, that has a child component called SessionComponent with @SessionScope. In SessionComponent, it has two child components, that is scoped with @RequestScope, named FooRequestComponent and BarRequestComponent, which is considered sibling to each others. This compiles find, as FooRequestComponent and BarRequestComponent doesn't have direct ancestry line between them.

@Singleton @Component
interface RootComponent2 {
    val sessionComponent: SessionComponent.Builder
}

@SessionScope @Subcomponent
interface SessionComponent {
    val fooRequestComponent: FooRequestComponent.Builder // OK
    val barRequestComponent: BarRequestComponent.Builder // OK

    @Subcomponent.Builder
    interface Builder {...}
}

@RequestScope @Subcomponent
interface FooRequestComponent  {...}

@RequestScope @Subcomponent
interface BarRequestComponent {...}
None

Subcomponents for encapsulation

The other benefit of Subcomponent is, it could be used to encapsulate its objects (in the Subcomponent).

Non-Encapsulation

Imagine if Database is dependent on DatabaseScheme and DatabaseConnectionPool, and all of them are provided by the within the same component, then DatabaseConnectionPool and DatabaseScheme could be easily accessible. This is not ideal.

@Singleton
@Component(modules = [DatabaseModule::class])
internal interface ApplicationComponent {
    val database: Database
    val databaseScheme: DatabaseSchema
    val databaseConnectionPool: DatabaseConnectionPool
}

@Module
class DatabaseModule {

    @NumberOfCores
    @Provides
    fun getNumberOfCore() = 10

    @Provides
    fun provideDatabaseConnectionPool(
        @NumberOfCores concurrencyLevel: Int)
        : DatabaseConnectionPool {
        return DatabaseConnectionPool(concurrencyLevel)
    }

    @Provides
    fun provideDatabaseSchema(): DatabaseSchema {
        return DatabaseSchema()
    }

    @Provides
    @Singleton
    fun provideDatabase(
        databaseSchema: DatabaseSchema,
        databaseConnectionPool: DatabaseConnectionPool)
        : Database {
        return Database(databaseSchema, databaseConnectionPool)
    }
}
None

Encapsulation

However, we can encapsulate those dependence into a Subcomponent (i.e. DatabaseComponent) as per the code below. This will hide away DatabaseScheme and DatabaseConnectionPool from being accessible by others other than Database itself.

@Singleton
@Component(modules = [DatabaseModule::class])
internal interface ApplicationComponent {
    val database: Database
    val databaseScheme: DatabaseSchema // Error
    val databaseConnectionPool: DatabaseConnectionPool // Error
}

@Module(subcomponents = [DatabaseComponent::class])
class DatabaseModule {

    @NumberOfCores
    @Provides
    fun getNumberOfCore() = 10

    @Provides
    @Singleton
    fun provideDatabase(
        @NumberOfCores numberOfCores: Int,
        databaseComponentBuilder: DatabaseComponent.Builder)
        : Database {
        return databaseComponentBuilder
            .databaseImplModule(
                DatabaseImplModule(numberOfCores / 2))
            .build()
            .database()
    }
}

@Module
class DatabaseImplModule(private val concurrencyLevel: Int) {
    @Provides
    fun provideDatabaseConnectionPool(): DatabaseConnectionPool {
        return DatabaseConnectionPool(concurrencyLevel)
    }

    @Provides
    fun provideDatabaseSchema(): DatabaseSchema {
        return DatabaseSchema()
    }

    @PrivateToDatabase
    @Provides
    fun provideDatabase(
        databaseSchema: DatabaseSchema,
        databaseConnectionPool: DatabaseConnectionPool): Database {
        return Database(databaseSchema, databaseConnectionPool)
    }
}

@Subcomponent(modules = [DatabaseImplModule::class])
interface DatabaseComponent {
    @PrivateToDatabase
    fun database(): Database

    @Subcomponent.Builder
    interface Builder {
        fun databaseImplModule(
             databaseImplModule: DatabaseImplModule): Builder
        fun build(): DatabaseComponent
    }
}
None

Defining subcomponents with abstract factory methods

As mentioned in the Adding Subcomponent to Parent section above, we could have abstract factory methods (or interface property in Kotlin) that return the subcomponent

@Singleton
@Component(modules = [ServerModule::class])
    val requestComponent: RequestComponent
}

Module with parameter

Assuming if the Subcomponent's module (i.e. RequestModule) need to have argument

@Module
class RequestModule(private val data: Data) {
    @Provides
    fun getRequestHandler() = RequestHandler(data)
}

@Subcomponent(modules = [RequestModule::class])
interface RequestComponent {
    val requestHandler: RequestHandler
}

Then we could add the parameter to the abstract function (can't use Interface Property of Kotlin anymore in this case)

@Singleton
@Component
interface ServerComponent {
    fun requestComponent(requestModule: RequestModule)
        : RequestComponent
}

Sharing module with parent

Imagine if both the Parent and the Subcomponent having the same module. The Subcomponent will just take the parent's component.

@Module
class ShareModule(private val data: Data) {
    @Provides
    fun getRequestHandler() = RequestHandler(data)
}

@Subcomponent(modules = [ShareModule::class])
interface RequestComponent {
    val requestHandler: RequestHandler
}

@Singleton
@Component(modules = [ShareModule::class])
interface ServerComponent {
    val requestComponent: RequestComponent
}
fun main() {
    val requestComponent = DaggerServerComponent
        .builder()
        .shareModule(ShareModule(Data("Parent Module")))
        .build()
        .requestComponent
}
None

Return the Subcomponent's builder instead

In the parent component, other than returning the subcomponent, we could return the builder instead as shown in example code below. This would make it possible to use the subcomponent's builder provided parameter (e.g. Module creation, @BindsInstance data etc) .

@Singleton
@Component(modules = [ServerModule::class])
interface ServerComponent {
    val requestComponentBuilder: RequestComponent.Builder
}

Comparison consideration

Should we return the Builder or not? Should we consider using Abstract Factory Method or just use the subcomponent parameter in the Parent's Module?

Below are two blogs for more reading any comparing the differences.

Details

Extending multibindings

Multibindings is a feature in Dagger2 that allow binding of instances into a Set or Map of Data that could be provided/injected into its target.

Subcomponent has enable Multibindings feature a different (or rather super) set/map of data in the subcomponent, than what the parent provides.

Illustrated in the diagram below, the parent provide only string1 and string2. For the same Set<String>, within the child's scope (i.e. Subcomponent Scope), it provides string1, string2, string3 and string4. This enables each subcomponent to provides different set of Set<String>

None
,

For more about Multibindings, refers to the blog below.

Repeated Module in Subcomponent (Not supported Error)

If a required module by the subcomponent, already provided by the Parent, the subcomponent shouldn't build its own subcomponent. It will either compile error or clash during run time depending on how the builder is called.

Below are 3 examples

@Module
class RepeatedModule

@Component(modules = [RepeatedModule::class])
interface ComponentRoot {
    val componentOne: ComponentOne
    // Compile Error
    // As a repeated module clash with what the parent provided
    fun componentTwo(repeatedModule: RepeatedModule): ComponentTwo 
    val componentThreeBuilder: ComponentThree.Builder
}

@Subcomponent(modules = [RepeatedModule::class])
interface ComponentOne

@Subcomponent(modules = [RepeatedModule::class])
interface ComponentTwo

@Subcomponent(modules = [RepeatedModule::class])
interface ComponentThree {
    @Subcomponent.Builder
    interface Builder {
        fun repeatedModule(repeatedModule: RepeatedModule): Builder
        fun build(): ComponentThree
    }
}

fun main() {
    val componentRoot = DaggerComponentRoot.create()
    componentRoot.componentThreeBuilder
        // UnsupportedOperationException!
        // As a repeated module clash with what the parent provided
        .repeatedModule(RepeatedModule()) 
        .build()

    componentRoot.componentOne
}
None

Hope this provided a clearer understanding of what Subcomponent is and how it function.

The working code (in Kotlin) is available below

Thanks for reading. You could check out my other topics here.

Follow me on medium, Twitter, Facebook or Reddit for little tips and learning on mobile development etc related topics. ~Elye~