Dependency Injection

From bibbleWiki
Jump to navigation Jump to search

Introduction

Dependency Injection or DI is when we provides the things we need into another object.
DI Intro.png
Originally in OO is was thought better to hide the internal objects and create them inside the object.

class Car {
   Engine engine
   Wheels wheels;

   Car() {
      engine = new Engine()
      wheels = new Wheels()
   }

   void drive() {
     // chug chug
   }

}


For Dependency Injection we can provide the prebuilt wheels and engine via the constructor

class Car {
   Engine engine
   Wheels wheels;

   Car(Engine engine, Wheels wheels) {
       this.engine = engine
       this.wheels = wheels
   }

   void drive() {
     // chug chug
   }

}

Why Dagger?

So we taking our example above we can now do.

..
   val engine = new Engine()
   val wheels = new Wheels()
    
   val car = new Car(engine, wheels)
..


Looks simple enough? Well lets add a few more parts

..

   val block = new Block()
   val cylinder = new Cylinder()
   val rims = new Rims()

   val engine = new Engine(block, cylinder)
   val wheels = new Wheels(rims)
    
   val car = new Car(engine, wheels)
..


Dagger exists to help manage the dependencies in terms of

  • dependencies
  • ordering
  • construction

With dagger this becomes

   val carComponent = DaggerCarComponent.create()
   val car = component.getCar()

So here we have a Directed Acyclic Graph' of our car or DAG :) DI DAG Graph.png

Component and Inject

Gradle Considerations

For both we needed

    implementation 'com.google.dagger:dagger:2.31.2'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.31.2'

However for kotlin we also needed

plugins {
...
    id 'kotlin-kapt'
}

// Dependencies
    kapt 'com.google.dagger:dagger-compiler:2.31.2'

Constuctor Injection

We need to

  • Specify @Component interface
  • Specify @Inject on the class to inject and its dependants

This compiles and creates our factory (builder) which is Dagger<Interface name>, in this case DaggerCarComponent. From there we can call getCar() to get our instance.
So we can now put the theory into practice.Here is our Car class.

class Car @Inject  constructor(private var engine: Engine, private var wheels: Wheels) {

   private const val TAG = "Car"

   fun drive() {
       Log.d(TAG, "driving...")
   }
}

And the Wheel and Engine

class Wheels @Inject  constructor()
class Engine @Inject  constructor()

And an interface to allow us to get the Car

@Component
interface CarComponent {

    fun getCar() : Car
}

Now we can create the Car with

...
    private lateinit var car: Car
...

        val component: CarComponent = DaggerCarComponent.create()
        car = component.getCar()
        car.drive()

Field Injection

For a class, in our case MainActivity, we can inject fields, so we can pass a class and have its field created. To do this all we need to do is

  • Create a function on the component interface
  • Add @Inject to the fields
  • Create the component and
  • Call the interface function
@Component
interface CarComponent {

    fun inject(activity:MainActivity)
}

And in the MainActivity

class ...
    @Inject lateinit var car: Car
...
        val component: CarComponent = DaggerCarComponent.create()
        component.inject(this)
        car.drive()
...

Method Injection

We can inject into methods as well but this is not very component. An example might be when you are passing the yourself to a method argument. e.g.

class Car ...
    @Inject 
    fun enableRemote(remote: Remote) {
        remote.setListener(this)
    }
...

And in the remote class

class Remote @Inject constructor() {

    private const val TAG = "Remote"
    
    fun setListener(Car car) {
        Log.d(TAG, "Remote connected")
    }
}

Kotlin vs Java

On Java the interface fails to compile with Missing/Binding however on Kotlin this is allowed. Clearly it would be ideal to identify issues in both.

Modules, Provides and Bind

Modules, Provides

Modules are a way to provide a instantiated classes together under one name. It consists of factory functions annotated with Provides.

@Module
object WheelsModule {
    @Provides
    fun provideRims(): Rims {
        return Rims()
    }

    @Provides
    fun provideTyres(): Tyres {
        val tyres = Tyres()
        tyres.inflate()
        return tyres
    }

    @Provides
    fun provideWheels(rims: Rims?, tyres: Tyres?): Wheels {
        return Wheels(rims!!, tyres!!)
    }
}

The module, or modules can then be associated with a component using the modules keyword followed by a list of module names.

@Component (modules = [WheelsModule::class])
interface CarComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)
}

Bind

Bind allows you to provide multiple implementation for an interface. In our example we could have a diesel and a petrol engine.
Let's create the interface

interface Engine {
    fun start()
}

And create the diesel class

class DieselEngine: Engine  {

    @Inject constructor()

    private val TAG = "DieselEngine"

    override fun start() {
        Log.d(TAG, "starting Diesel Engine")
    }
}

And create the petrol class

class PetrolEngine: Engine  {

    @Inject constructor()

    private val TAG = "PetrolEngine"

    override fun start() {
        Log.d(TAG, "starting Petrol Engine")
    }
}

Now we need to create a modules, like the Diesel and Petrol implementation these are identical so only showing the petrol module. The bind keyword signifies this is the implementation to use for our engine

@Module
abstract class PetrolEngineModule {
    @Binds
    abstract fun bindEngine(engine: PetrolEngine) : Engine
}

We say this in the component. Obviously or perhaps not because I am writing this, we cannot put both implementation in our declaration of the component. This approach works well for the testing too.

@Component (modules = [WheelsModule::class, PetrolEngineModule::class])
interface CarComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)
}

Inject values At Runtime

Introduction

There are 3 approaches to passing values at runtime

  • Passing values to the module when creating Component
  • Passing values to the module and injecting instance
  • Passing values using Component.Builder Interface

Passing values to the module when creating Component

All of the examples above are static factory classes. In the real world we will need to probably pass configuration at runtime to the some of the objects. To do this we can provide the value to the module and inject it into the class. Let's say we want to configure horse power to the diesel engine at runtime. We will walk through this in reverse order.

  • Add value to constructor
  • Remove the inject keyword from the class
  • Add the Parameter to the module and pass to the class
  • Change creation of Component to call the constructor to pass in the runtime value
class DieselEngine: Engine  {

    DieselEngine(private val horsePower: Int)

    private val TAG = "DieselEngine"

    override fun start() {
        Log.d(TAG, "starting Diesel Engine with Horse Power ${horsePower}")
    }
}

This means we can no longer inject this class so we need to instantiate it in the module. So the Binds used above can no longer be used and we need to go back to the @Provides annotation and return a new instance. We add a constructor to the module to provide the runtime value for horsepower

@Module
class DieselEngineModule constructor(private val horsePower: Int){

    @Provides
    fun providesEngine(): Engine {
        return DieselEngine(horsePower )
    }
}

This causes the create method in the activity to be undefined. This is because the create method expects all of the class to be instantiated with no run time arguments. Now we need to use the build command.

        val component: CarComponent = DaggerCarComponent
            .builder()
            .dieselEngineModule(DieselEngineModule(100))
            .build()

Passing to the module and injecting instance

Instead of passing the value from the module to instance we can use injection from the module into the provider. This, to me, looked awful. Basically the module will look for the type marked with @Provides and use that value regardless of whether it is correct. I.E. the only tie between the provideEngine and provideHorsePower is that it is an Int

  • Add @Provides to an Int on the module, called horsePower but could be called pangalactic gargleblaster
  • Change Factory method to receive an instance and return it
  • Change the class back to @Inject


Add @Provides to an Int on the module

    @Provides
    fun horsePower(): Int {
        return horsePower
    }

Change Factory method to receive an instance

    @Provides
    fun providesEngine(dieselEngine: DieselEngine): Engine {
        return dieselEngine
    }

And add the @Inject onto the Diesel Class

Passing values using Component.Builder Interface

In this approach we create classes with parameters annotated with @Named and match them to the parameters in the Component.Builder interface.

Component with Parameters Usage

What we are trying to achieve is to create a Component and provide parameters at runtime. Before looking at how this implemented lets look at what the usage will look like.

class MainActivity : AppCompatActivity() {
...
    override fun onCreate(savedInstanceState: Bundle?) {
...
        val component: CarComponent = DaggerCarComponent
            .builder()
            .horsePower(100)
            .engineCapacity(200)
            .build()


To achieve this we

  • Create a Class (with @Named constructor values)
  • Create a Module (to allow polymorphism)
  • Create a Component (with @Named @BindInstance values)

Create Class (@Inject/@Named)

We create a class as we would normally with the two parameters we need. With each value we associate a @Named annotation. This should be a unique label for each parameter and is used by the @Component to identify which @BindInstance should be used when creating the class instance.

Note Dagger is not aware of parameter names in constructors only the type of the argument. The @Named is used to differentiate between two constructor arguments with the same type. In our case int.

class DieselEngine @Inject constructor(
    @Named("Horse Power") private val horsePower: Int,
    @Named("Engine Capacity") private val engineCapacity: Int) : Engine  {

    private val TAG = "DieselEngine"

    override fun start() {
        Log.d(TAG, "starting Diesel Engine with\n Horse Power ${horsePower}\n Capacity ${engineCapacity}\n" +
                "  ")
    }
}

Create Module (@Module)

This is now required is we are not going use polymorphism. We simply create

  • an abstract class with the @Module annotation
  • an abstract function with the @Bind which returns an instance of the base class
@Module
abstract class DieselEngineModule {

    @Binds abstract fun bindEngine(dieselEngine: DieselEngine): Engine
}

Create Component (@Component/@Component.Builder/@BindInstance

To create the component we need to

  • Create a Component.Builder Interface
  • Complete Component

Create a Component.Builder Interface

This typically will be inside of the component. It is shown here to break down possibly the most complicated part of the Dagger process. The interface should contain

  • One function which returns Builder for each parameter we want our component to support
    • Each should be annotated with @BindInstance
    • Each should be annotated with @Named
  • One function which returns Builder for dependency we want to support
  • function build() which returns the type

Note we do not have a dependency in this case. See example #Create Activity Level Component (@Component/@PerActivity).

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun horsePower(@Named("Horse Power") horsePower: Int): Builder

        @BindsInstance
        fun engineCapacity(@Named("Engine Capacity") engineCapacity: Int): Builder

        fun build(): CarComponent
    }

Complete Component

Now we have the Component.Builder we can create the component. This includes

  • Annotate the interface with @Component
  • List the modules associated with the @Component
  • Create an accessor for the Class
@Component (modules = [WheelsModule::class, DieselEngineModule::class])
interface CarComponent {

    fun getCar() : Car

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun horsePower(@Named("Horse Power") horsePower: Int): Builder

        @BindsInstance
        fun engineCapacity(@Named("Engine Capacity") engineCapacity: Int): Builder

        fun build(): CarComponent
    }

Singleton Or NOT

First off the @Singleton annotation are scoped at Component level. I.E. two components, two instances. The Singleton annotation tell Dagger to create only one instance of class when used. For this to work you have to also specify the annotation on the component too.
Below we create new class Driver.

class Driver @Inject constructor()

We amend the Car Class to add the driver

class Car @Inject  constructor(
    private var driver: Driver,
    private var engine: Engine, 
    private var wheels: Wheels) {

   private val TAG = "Car"

   fun drive() {
       engine.start()
       Log.d(TAG, "driving with driver ${driver}")
   }
...

Now when we create two instances of car we see two difference instances of driver.

2021-02-02 00:45:03.836 5255-5255/com.example.daggerexample D/Car: driving with driver com.example.daggerexample.Driver@1d9542d
2021-02-02 00:45:13.587 5255-5255/com.example.daggerexample D/DieselEngine: starting Diesel Engine with
     Horse Power 100
     Capacity 200
      
2021-02-02 00:45:13.588 5255-5255/com.example.daggerexample D/Car: driving with driver com.example.daggerexample.Driver@8fcbd62

To make only one instance all we need to do is add @Singleton to the Class

@Singleton
class Driver @Inject constructor()

And the Component

@Singleton
@Component (modules = [WheelsModule::class, DieselEngineModule::class])
interface CarComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)
...

So to be clear even with the annotation @Singleton is used, if we create two components we get to instances regardless of the @Singleton keyword.

        val component1: CarComponent = DaggerCarComponent
            .builder()
            .horsePower(100)
            .engineCapacity(200)
            .build()

        val component2: CarComponent = DaggerCarComponent
            .builder()
            .horsePower(100)
            .engineCapacity(200)
            .build()

        component1.getCar().drive()
        component2.getCar().drive()

This obviously also applies when you rotate the screen as the activity, along with the components are destroyed and rebuilt.

Scope

Introduction

We found out that @Singleton did not mean it was a singleton. In actual fact we are responsible for creating scope. The annotation does not really implement anything aside from the grouping. Below are two examples,

  • Create Application Scoped Component
  • Create Activity Scoped Component with Application Scoped Driver

I found the second example a bit hard going and wondered if the tool is getting more complex than the problem we are trying to solve.

Create Application Level Component

Create App Level Scope Interface

We do not need to do this because we are using the built in label @Singleton provided by the java library and not Dagger.

Create App Level Classes

We now can define the Dummy Driver class with no annotations. This is to simulate a third-party class.

// Empty to assume it is a third-party class.
class Driver

Create App Level Module (@Module/@Provides/@Singleton)

In reality we can use the @Inject on our Driver Class. However to demonstrate how this might work with a third-party where you cannot do this, we create a module to wrap it. We also add the scope annotation @Singleton to signify this is at application level.

@Module
object DriverModule {

    @Singleton
    @Provides
    fun provideDriver(): Driver {
        return Driver()
    }
}

Create App Level Component (@Component/@Singleton)

Create App Level Component by adding the @Component and adding the modules we wish to be associated with this component. We annotate is with the Scope label @Singleton. Remember this could be @Avon, it is a label, but given it is going to be a Singleton it makes sense. Note this will contain our App Level classes

@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
    fun getDriver() : Driver
}

Implement App Level

Introduction

This is not a Dagger thing, this is an Android thing. Create are creating something which is at App Level. For any class, if we want it at app level we would need to do this

Create Application Class

We create and Application Class which can be used with our App. This class will be created once and therefore implement the Singleton pattern. Now we can provide a accessor to the Component for use across the App.

class ExampleApp : Application() {

    private lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent.create()

    }

    fun getAppComponent() :AppComponent {
        return appComponent
    }
}

Use Application Class Member

We can now use our App Level Component, in this case, in an Activity.

...
class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val driver: AppComponent =
                (getApplication() as ExampleApp).getAppComponent()

        Log.d(TAG, "onCreate: Driver Created ${driver}")
...

Add Application Class To Manifest

For Android to use our Application Class we need to tell the Manifest about it

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.daggerexample">

    <application
        android:name=".ExampleApp"
...

Create Activity Level Component

Create Activity Level Scope Interface

@Scope
@Retention(RetentionPolicy.RUNTIME)
@Documented
annotation class PerActivity

Create Activity Level Classes (@Inject/PerActivity)

In our case we are going to use a class with @Inject, the car class. But we could equally have used a Third-Party class and put a module around it. Because there is no module we need to add the Activity Scope annotation @PerActivity to the class

@PerActivity
class Car @Inject  constructor(
    private var driver: Driver,
    private var engine: Engine,
    private var wheels: Wheels) {

   private val TAG = "Car"

   fun drive() {
       engine.start()
       Log.d(TAG, "driving with driver ${driver}")
   }

    @Inject
    fun enableRemote(remote: Remote) {
        remote.setListener(this)
    }
}

Create Activity Level Module (@Module/@Provides/@PerActivity)

If we were dealing with an a Third-Party Class we could wrap this in a module which would be responsible for providing an instance of the class.

Create Activity Level Component (@Component/@PerActivity)

Create Activity Level Component by

  • adding the @PerActivity annotation, scope label
  • adding the @Component annotation
  • adding the dependencies to make this component
  • adding the modules we wish to be associated with this component.
  • add Builder Interface parameters
@PerActivity
@Component (dependencies = [AppComponent::class], modules = [WheelsModule::class, DieselEngineModule::class])
interface ActivityComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun horsePower(@Named("Horse Power") horsePower: Int):Builder

        @BindsInstance
        fun engineCapacity(@Named("Engine Capacity") engineCapacity: Int): Builder

        fun appComponent(appComponent: AppComponent) : Builder
        
        fun build(): ActivityComponent
    }
}

In this example we have used dependencies in the @Component declaration. This means in the @Component.Builder we also need to provide a function signature to inject the dependency into the builder. Not doing so will result in the error.

@Component.Builder is missing setters for required modules or components: [com.example.daggerexample.AppComponent]

Usage of Activity Level Component

When we create the Activity Component we are depend on the Application Component so we have to pass this to the component constructor.

        var activityComponent = DaggerActivityComponent
                .builder()
                .horsePower(100)
                .engineCapacity(200)
                .appComponent((getApplication() as ExampleApp).getAppComponent())
                .build()

        var car = activityComponent.getCar()
        car.drive()

Subcomponents

Introduction

In the previous example we made a component where had a dependency. In order for the component to access the classes within the dependency it was necessary to have an accessor on the dependant component. E.g.

@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
    fun getDriver() : Driver
}

Without this accessor it would not be possible to access the driver class. To overcome this we can make this a Subcomponent of a component

Creating a Subcomponent Passing values to the Module

Creating Subcomponent Objects

To create a subcomponent we need to create the following

  • Create Classes
  • Create Modules
  • Create Subcomponent

Create Classes

We already have our classes from previous sections above. For completeness here is the DieselEngine class

class DieselEngine(private val horsePower: Int) : Engine  {

    private val TAG = "DieselEngine"

    override fun start() {
        Log.d(TAG, "starting Diesel Engine with Horse Power ${horsePower}")
    }
}

We will be using the existing classes for the rest of the Subcomponent

Create Modules

For our example we will be using the existing modules. For the class above we will be passing the horsePower to the module at the point the component is constructed. For completeness it is provided here.

@Module
public class DieselEngineModule(val horsePower: Int) {

    @Provides
    fun provideHorsePower(): Int {
        return horsePower
    }

    @Provides fun provideEngine(): Engine {
        return DieselEngine(horsePower)
    }
}

Create Subcomponent

To create the Subcomponent, we

  • Add Subcomponent annotation
  • Specify the modules associated with our subcomponent
@PerActivity
@Subcomponent (modules = [WheelsModule::class, DieselEngineModule::class])
interface ActivityComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)
}

Creating a Parent Component

In the parent component we need to

  • provide modules associated with this component
  • provide an accessor to Subcomponents
    • provide arguments to the Subcomponents

We have to pass all modules which are not abstract or have a default constructor.

@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
    fun getActivityComponent(
        dieselEngineModule: DieselEngineModule) 
        :ActivityComponent
}

In our example our component has a subcomponent ActivityComponent. The module DieselEngineModule is not abstract and does not have a default constructor so we do need to pass this to the accessor.

Usage Demonstration

We can now construct an instance of the component, we do need to pass the horsepower to the DieselModule.

class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"
    
    @Inject lateinit var car1 : Car
    @Inject lateinit var car2 : Car
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var component: ActivityComponent =
                (application as ExampleApp)
                        .getAppComponent()
                        .getActivityComponent(DieselEngineModule(120))

        component.inject(this)
        car1.drive()
        car2.drive()
...

Creating a Subcomponent Passing values using Component.Builder Interface

Creating Subcomponent Objects

To create a subcomponent we need to create the following

  • Create Classes
  • Create Modules
  • Create Subcomponent

Create Classes

We already have our classes from previous sections above. For completeness here is the PetrolEngine class which uses Component.Builder to create the class.

class PetrolEngine @Inject constructor(
        @Named("Horse Power") private val horsePower: Int,
        @Named("Engine Capacity") private val engineCapacity: Int) : Engine  {

    private val TAG = "PetrolEngine"

    override fun start() {
        Log.d(TAG, "starting Petorl Engine with\n " +
                "Horse Power ${horsePower}\n " +
                "Capacity ${engineCapacity}\n" +
                "  ")
    }
}

We will be using the existing classes for the rest of the Subcomponent

Create Modules

For our example we will be using the existing modules. For the class above we will be using the Component.Builder so the module will just be an abstract class

@Module
abstract class PetrolEngineModule {
    @Binds
    abstract fun bindEngine(engine: PetrolEngine) : Engine
}

Create Subcomponent

To create the Subcomponent, we

  • Create a Subcomponent Interface
  • Complete the Subcomponent
Create a Subcomponent.Builder Interface

This typically will be inside of the component. It is shown here to break down possibly the most complicated part of the Dagger process. The interface should contain

  • One function which returns Builder for each parameter we want our component to support
    • Each should be annotated with @BindInstance
    • Each should be annotated with @Named
  • function build() which returns the type
    @Subcomponent.Builder
    interface Builder {

        @BindsInstance
        fun horsePower(@Named("Horse Power") horsePower: Int): Builder

        @BindsInstance
        fun engineCapacity(@Named("Engine Capacity") engineCapacity: Int): Builder

        fun build(): CarComponent
    }
Complete the Subcomponent
@PerActivity
@Subcomponent (modules = [WheelsModule::class, DieselEngineModule::class])
interface ActivityComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)
}

Creating a Parent Component

In the parent component we need to

  • provide modules associated with this component
  • provide the Builder method for the subcomponent

For Subcomponent which use the Subcomponent.Builder, they do not have arguments passed directly. We just need to return the builder for this.

@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
    fun getActivityComponentBuilder(): ActivityComponent.Builder
}

Usage Demonstration

We can now construct an instance of the component, we do need to pass the horsepower and engineCapacity via the ActivityComponentBuilder.

class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"
    
    @Inject lateinit var car1 : Car
    @Inject lateinit var car2 : Car
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var component: ActivityComponent =
                (application as ExampleApp).getAppComponent()
                        .getActivityComponentBuilder()
                        .horsePower(100)
                        .engineCapacity(200)
                        .build()

        component.inject(this)
        car1.drive()
        car2.drive()
...

Subcomponent Factory

Instead of using the @Subcomponent.Builder we can now use the @Subcomponent.Factory. This is a great improvement and allows for compile time checking. To use this we need to amend the

  • Subcomponent
  • Compoent
  • Usage

Amend Subcomponent

To implement this we need to

  • Rename Subcomponent.Builder for a Subcomponent.Factory Interface
  • Create a function which returns an instance usually called create
  • Add all of the @BindInstance variable to the create function
@PerActivity
@Subcomponent (modules = [WheelsModule::class, PetrolEngineModule::class])
interface ActivityComponent {

    fun getCar() : Car

    fun inject(activity: MainActivity)

//    @Subcomponent.Builder
//    interface Builder {
//
//        @BindsInstance
//        fun horsePower(@Named("Horse Power") horsePower: Int):Builder
//
//        @BindsInstance
//        fun engineCapacity(@Named("Engine Capacity") engineCapacity: Int): Builder
//
//        fun build(): ActivityComponent
//    }

    @Subcomponent.Factory
    interface Factory {
        fun create(
                @BindsInstance @Named("Horse Power") horsePower: Int,
                @BindsInstance @Named("Engine Capacity") engineCapacity: Int
        ) :ActivityComponent
    }

}

Amend Component

We need to amend the Component to reference the Factory function instead of Builder.

@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
//    fun getActivityComponentBuilder(): ActivityComponent.Builder
    fun getActivityComponentFactory(): ActivityComponent.Factory
}

Amend Usage Demonstration

Now we need to amend the Usage to use the Factory function. The benefit of this approach is that horsePower and engineCapacity are checked at compile time.

class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"
    
    @Inject lateinit var car1 : Car
    @Inject lateinit var car2 : Car
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

//        var component: ActivityComponent =
//                (application as ExampleApp).getAppComponent()
//                        .getActivityComponentBuilder()
//                        .horsePower(100)
//                        .engineCapacity(200)
//                        .build()

        var component: ActivityComponent =
                (application as ExampleApp).getAppComponent()
                        .getActivityComponentFactory()
                        .create(100, 200)

        component.inject(this)
        car1.drive()
        car2.drive()
...

Component Factory

Now let look at a component Factory. In this example we will create a component which requires a value at runtime. To use this we need to amend the

  • Class (Just to pretend it needs a value at runtime)
  • Module (To support the Class)
  • Compoent
  • Usage

Amend Class

Let's take the Driver class and add a parameter. We could of course add @Inject to avoid this but we are pretending it is a Third-Party class

// Empty to assume it is a third-party class.
class Driver(private val driverName: String)

Amend Module

@Module
class DriverModule(private val driverName : String) {

    @Singleton
    @Provides
    fun provideDriver(): Driver {
        return Driver(driverName)
    }
}

Amend Component

To implement this we need to create an interface annotated with Component.Factory. This interface needs

  • A function which returns a Factory method for any sub classes
  • A function which returns an instance of the component class
  • Have arguments for each module which is
    • not abstract and
    • cannot cannot be constructor with no arguments
@Singleton
@Component(modules = [DriverModule::class])
interface AppComponent {
    fun getActivityComponentFactory(): ActivityComponent.Factory

    @Component.Factory
    interface Factory {
        fun create(driverModule: DriverModule) : AppComponent
    }
}

Amend Usage Demonstration

Now we need to amend the App Component to pass the Driver name to the Driver Module.

class ExampleApp : Application() {

    private lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent
                .factory()
                .create(DriverModule("Bill"))
    }

    fun getAppComponent() :AppComponent {
        return appComponent
    }
}