Dependency Injection: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 273: Line 273:
</syntaxhighlight>
</syntaxhighlight>
=Inject values At Runtime=
=Inject values At Runtime=
==Introduction==
There are 3 approaches to passing values at runtime
*Passing to the module and constructing instance
*Passing to the module and injecting instance
*Component Builder and BindsInstance
==Passing to the module and constructing instance==
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.
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
*Add value to constructor
Line 309: Line 315:
             .build()
             .build()
</syntaxhighlight>
</syntaxhighlight>
==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
<br>
Add @Provides to an Int on the module
<syntaxhighlight lang="kotlin">
    @Provides
    fun horsePower(): Int {
        return horsePower
    }
</syntaxhighlight>
Change Factory method to receive an instance
<syntaxhighlight lang="kotlin">
    @Provides
    fun providesEngine(dieselEngine: DieselEngine): Engine {
        return dieselEngine
    }
</syntaxhighlight>
And add the @Inject onto the Diesel Class
==Component Builder and BindsInstance==
So without doubt this gets worse and worse. As mentions above, dagger, at it's lowest form looks for a type and uses it. To implement this we need to think of the values passed to the @Component as a dictionary of values. This approach attempts to provide some comfort we are using the right values.
<br>
On the @Component we specify a @Component.Builder annotation. This is an internal interface which specifies functions to return values.
<syntaxhighlight lang="kotlin">
</syntaxhighlight>
On each instance of the class we need to tie the @Named value to the constructor argument.

Revision as of 10:16, 1 February 2021

Introduction

Dependency Injection or DI is when we provides the things we need into another object.

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 :)

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 to the module and constructing instance
  • Passing to the module and injecting instance
  • Component Builder and BindsInstance

Passing to the module and constructing instance

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

Component Builder and BindsInstance

So without doubt this gets worse and worse. As mentions above, dagger, at it's lowest form looks for a type and uses it. To implement this we need to think of the values passed to the @Component as a dictionary of values. This approach attempts to provide some comfort we are using the right values.
On the @Component we specify a @Component.Builder annotation. This is an internal interface which specifies functions to return values.

On each instance of the class we need to tie the @Named value to the constructor argument.