Dependency Injection: Difference between revisions
Line 505: | Line 505: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===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==== | |||
====Add Application Class To Manifest==== | |||
===Create Interface=== | ===Create Interface=== |
Revision as of 22:18, 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.
In this example we want to pass two values, the horse power and the engine capacity at the time of creating a Car component.
Usage Implementation
For this example we are going to work backwards. This is what the instantiation will look like
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val component: CarComponent = DaggerCarComponent
.builder()
.horsePower(100)
.engineCapacity(200)
.build()
The Component Implementation
To provide the two functions, horsePower and engineCapacity on the component, we need to create an internal interface. Each of the functionsOn the @Component we specify a @Component.Builder annotation. This is an internal interface. Each function must return a Builder object and be annotated with the @BindsInstance.
@Component (modules = [WheelsModule::class, DieselEngineModule::class])
interface CarComponent {
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 build(): CarComponent
}
The Module Implementation
Now we have all of the required data to create an instance of the Diesel Engine, we can just make the Diesel Module Class an abstract class as we did above.
@Module
abstract class DieselEngineModule {
@Binds abstract fun bindEngine(dieselEngine: DieselEngine): Engine
}
The Injected Class Implementation
For the Class implementation we now have all of the data available to us. Unfortunately the function names on the @Component horsePower() and engineCapacity() mean nothing to this class. It is the @Named annotation which tells Dagger where to get the value for horsePower from.
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" +
" ")
}
}
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 Activity Scoped Component with Application Scoped Driver
Introduction
In this example we will work through creating two scopes of components, one at application level and one we Activity level. Originally the examples were to convert existing code. I struggled to understand the errors during the creation so I moved to creating first, the application level and then the activity level.
Application Level
Create App Level Dummy Class
We now can define the Dummy Driver class to be just a class and 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)
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)
Create App Level Component by adding the @Component and adding the modules we wish to be associated with this component. We label it @Singleton. Again this could be Avon 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
Add Application Class To Manifest
Create Interface
Let's create an interface to represent the level we are targeting.
@Scope
@Retention(RetentionPolicy.RUNTIME)
@Documented
annotation class PerActivity
Add Scope Annotation to Class for this Scope
So add the annotation. Remember we really control the scope, the annotation are for Dagger to tie up the dependencies.
@PerActivity
class Car @Inject constructor(
private var driver: Driver,
private var engine: Engine,
private var wheels: Wheels) {
...
Create Our Activity Level Component
With the example we just rename the existing CarComponent to ActivityComponent and add the @PerActivity annotation
@PerActivity
@Component (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 build(): ActivityComponent
}
}
Add All App Classes to App Component
We only have the driver class but this is what we are doing
@Singleton
@Component(modules = [Driver::class])
interface AppComponent {
fun getDriver() : Driver
}
Create Application Scope
It may be convenient to have component which is scoped to App. One way to achieve this is to create a Application Class of our one. For example
class ExampleApp : Application() {
private lateinit var carComponent: CarComponent
override fun onCreate() {
super.onCreate()
val component: CarComponent = DaggerCarComponent
.builder()
.horsePower(100)
.engineCapacity(200)
.build()
}
fun getCarComponent() :CarComponent {
return carComponent
}
}
We need to change the manifest to have our class the as base Application class.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.daggerexample">
<application
android:name=".ExampleApp"
...
And finally use this class in the Activity to create the instances.
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val component: CarComponent =
(getApplication() as ExampleApp).getCarComponent()
component.inject(this)
car1.drive()
car2.drive()