Search code examples
kotlindagger-2dagger

Dagger 2: Exposing object from subcomponent in component


I have a situation where I have a component: CarComponent with scope @CarScope and a subcomponent DriverSubcomponent with scope @DriverScope. Basically car requires a driver and driver requires a helmet.

Here is the car component:

@CarScope
@Component
interface CarComponent {
    val driverComponentBuilder: DriverComponent.Builder
    fun getCar(): Car
    @Component.Builder
    interface Builder{
        fun build(): CarComponent
        @BindsInstance
        fun carName(@Named("CNAME")  name: String): Builder
        @BindsInstance
        fun driverName(@Named("DNAME")  driverName: String): Builder
    }
}

And a driver subcomponent:

@DriverScope
@Subcomponent(modules=[HelmetModule::class])
interface DriverComponent {
    fun getDriver(): Driver
    @Subcomponent.Builder
    interface Builder {
        fun build(): DriverComponent
    }
}

Helmet module:

@Module
interface HelmetModule {

    @Binds
    fun bindHelmet(whiteHelmet: WhiteHelmet): Helmet
}

And corresponding classes:

@CarScope
class Car @Inject constructor(@Named("CNAME") private val name: String, private val driver: Driver) {
    override fun toString(): String {
        return "Car: $name, Driver: $driver, hash: ${super.toString()}"
    }
}
class Driver @Inject constructor (@Named("DNAME") private val driverName: String, private val helmet: Helmet){
    override fun toString(): String{
        println("Driver Name: $driverName")
        println("Helmet info: $helmet")
        println (super.toString())
        return super.toString()
    }
}
interface Helmet {
    fun putOn(): Boolean
    fun takeOff(): Boolean
}
class WhiteHelmet @Inject constructor() : Helmet {
    override fun putOn(): Boolean {
        println("White Helmet is on")
        return true
    }

    override fun takeOff(): Boolean {
        println("White Helmet is off")
        return false
    }

    override fun toString(): String {
        return "White Helmet"
    }
}

I have noticed that this code will not compile unless I add (modules = [HelmetModule::class]) to CarComponent. It seems that when I call getCar(), it does not use Driver provided by the DriverComponent, but creates all required objects instead,

My goal would be to use Driver provided by the DriverComponent.

What are the ways to achieve this? Is the current behaviour related to the custom scopes I used?

Thanks. Leszek


Solution

  • DriverComponent is a subcomponent of CarComponent. CarComponent is a top-level component. This means:

    • You can create a CarComponent directly with its Builder or a factory method.
    • You cannot create a DriverComponent directly, but from a CarComponent instance, you can get a Builder that can return you a DriverComponent. You can repeat this process such that a CarComponent instance may have many DriverComponent instances that belong to it.
    • Any DriverComponent you create from a given CarComponent instance has access to that particular CarComponent instance's object graph; the Driver and Helmet have direct access to the SteeringWheel or whatever else might be in your CarComponent.
    • A CarComponent can't access the bindings within DriverComponent, because the CarComponent may have zero, one, or many DriverComponent instances that it has created.

    All of the above is true regardless of your scopes; you could remove them and it would still all be true. However, since you are using scopes:

    • A binding in DriverComponent that has @DriverScope will return the same instance wherever you inject it from within that DriverComponent instance.
    • A binding in CarComponent that has @CarScope will return the same instance wherever you inject it from within that CarComponent instance, from anywhere directly within the CarComponent or from any DriverComponent you create using CarComponent's Builder.

    If the above sounds correct to you—that a DriverComponent can only be created from within a CarComponent instance, after that CarComponent is created—then your use case matches the Dagger Subcomponents for encapsulation docs. All you need to do is bind a single @CarScope instance of DriverComponent, which you can create using the injectable DriverComponent.Builder. You can then confidently move HelmetModule and driverName onto your DriverComponent and its Builder respectively.

    @Provides @CarScope DriverComponent provideDriverComponent(DriverComponent.Builder builder) {
      return builder.build()
    }
    

    Or in Kotlin:

    @Provides @CarScope
    fun provideDriverComponent(builder: DriverComponent.Builder) = builder.build()
    

    Your Car won't be able to directly get to your Driver or their Helmet, but you can get to those by adding methods to your DriverComponent and getting them from that instance. You could even write a Provider that returns a Helmet in CarComponent by injecting your DriverComponent and returning driverComponent.helmet.

    If that doesn't look right—maybe your maybe your DriverComponent doesn't need any CarComponent bindings, and your Driver should be able to be created without a CarComponent instance—then you might need to avoid the Subcomponent and have your Driver or DriverComponent passed into your CarComponent.Builder.