Dependency Injection is one of essential things in Object Oriented Programming. Dependency injection concept origins from fifth principle of S.O.L.I.D. The principle behind it states that a class should not configure (or create) it’s own dependencies but instead its dependencies should be created by other class from outside. By doing so, we are able to make a class not tightly coupled with its own dependencies that in the end will make a class easier to be unit tested with its own class responsibility.
Since Kotlin
run on JVM, we can use one concept that already exists in JVM world to help us in building DI Library. To build dependency injection library in JVM world, we can use Java Reflection to construct dependencies of a class needed at runtime and inject into the class. First, I’m going to share a simple sample example of Android Dependency Injection which will be using MVP pattern for an UI Activity in Android. An Activity will have one presenter as its dependency to perform UI logics. For an example, we will name the activity as MainActivity
and presenter as MainPresenter
. MainPresenter
will be an Interface
and it will have its implementation in MainPresenterImpl
. As this goes, MainActivity
doesn’t need to know MainPresenter’s
implementation. As principles we always hold in OOP, we should always code against an interface instead of its concrete implementation.
class MainActivity: AppCompatActivity() { | |
lateinit var mainPresenter: MainPresenter | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
// inject mainPresenter here | |
mainPresenter.getData() | |
} | |
} |
MainActivity.kt
interface MainPresenter { | |
fun getData(): String | |
} |
MainPresenter.kt
class MainPresenterImpl: MainPresenter { | |
override fun getData(): String { | |
return "data" | |
} | |
} |
MainPresenterImpl.kt
Here’s the interesting part, we will create an Injector
class to help us in creating MainPresenter
instance and wiring it into MainActivity
. As this Injector
class is ideally a class that can be reused and should only created once when our App is running, we will use Kotlin
object
keyword to help us in creating one instance of Injector
(singleton) in our app.
object Injector { | |
fun inject() | |
} |
Injector.kt
As stated above, we need to make a class that will list out all dependencies that our entry point needed during the runtime. In short, we will name the class that functions that way as a Module
class. As our entry point is MainActivity
, we can create a class MainModule
that will help to provide all of the methods needed to create dependencies of MainActivity
which is currently only MainPresenter
(believe me things will get more interesting at the end of series). To make our Injector
recognizes that MainModule
is a Module
class, we need to create an abstraction using interface to restrict in compile time that MainModule
should implements InjectorModule
.
interface InjectorModule { | |
} |
InjectorModule.kt
class MainModule: InjectorModule { | |
fun provideMainPresenter(): MainPresenter { | |
return MainPresenterImpl() | |
} | |
} |
MainModule.kt
As we already created a class that will help us to list out functions to construct all MainActivity
dependencies, we can proceed to create logic that will run MainModule
methods and set all dependencies instances into MainActivity
field. In Java Reflection, we are able to list out all methods that a class defined by running this code Class<T>.getDeclaredMethods()
in Java
or Class<T>.declaredMethods
in Kotlin
. To make things safer during runtime, we should only execute methods that really functions as provider method that construct a dependency instance. In order to that, we need an unique identifier at the method that will tell us that it is a provider method. In this case, we will use annotation that will be placed above function that return a dependency instance.
@Retention(AnnotationRetention.RUNTIME) | |
@Target(AnnotationTarget.FUNCTION) | |
annotation class Provides |
Provides.kt
class MainModule: InjectorModule { | |
@Provides | |
fun provideMainPresenter(): MainPresenter { | |
return MainPresenterImpl() | |
} | |
} |
MainModule.kt
Now we can proceed in Injector
class to run MainModule
’s provider methods , cache it’s dependencies temporary and set it into MainActivity
using Field Reflection. Here’s the implementation.
object Injector { | |
fun <T : InjectorModule, R : Any> inject(kClass: KClass<T>, entryPointClass: R) { | |
// get all provider methods | |
val methods = kClass.java.declaredMethods | |
.filter { method -> method.isAnnotationPresent(Provides::class.java) } | |
// construct module instance | |
val moduleInstance = kClass.java.newInstance() | |
// construct and cache dependencies | |
val dependencies: MutableMap<Class<*>, Any> = mutableMapOf() | |
methods.forEach { method -> | |
dependencies[method.returnType] = method.invoke(moduleInstance) | |
} | |
// ToDo: inject dependencies | |
} | |
} |
Job Offers
In short, basically we make sure we only received required provider methods by checking if a method does contain Provides::class.java
annotation that will be executed later on. Once we already get the provider methods, we can begin constructing the dependencies instance by looping and execute the provider methods. As we construct the instance, we need to save it into a Map
using Class<*>
as key. Class<*>
key of MainPresenter
is MainPresenter::class.java
. As dependencies will only create once using Class<*>
is the best option currently as key for caching. To run provider methods that construct the dependencies, we only need to run method.invoke(moduleInstance)
. method.invoke(moduleInstance)
can accept argument using varargs
but in our case now we can leave it like this because MainPresenter
doesn’t have any dependencies yet. In this case, we need to provide moduleInstance
which is instance of InjectorModule
in our case it is MainModule
to run the provider method. In the end, all instances of dependencies will be stored inside dependencies: MutableMap<Class<*>, Any>
map. Now we’re left with injecting part.
Here comes the part of entryPointClass: R
in inject
method’s arguments. It plays an important part of injecting dependencies into a public field of a class. To get all fields of a class using Java Reflection, we can use Class<T>.fields
method in Kotlin
. But we try to run the code, we are getting literally all fields that an Activity contains. You will see result like this:
These aren’t great for us as we only need the fields that we’re going to inject the dependencies of MainActivity
. To solve this problem, we will use the same approach as provider method of Module
which is using Annotation as field identifier. In this case, we can make Inject
annotations that we will place above fields of MainActivity
that we will inject.
@Retention(AnnotationRetention.RUNTIME) | |
@Target(AnnotationTarget.FIELD, AnnotationTarget.CONSTRUCTOR) | |
annotation class Inject |
In current case, we only need AnnotationTarget.FIELD
, but we’re adding AnnotationTarget.CONSTRUCTOR
also for more complex DI case that we will discuss later on. After we’re done creating the Inject annotation, we can proceed to place @Inject
above fields that are dependencies of MainActivity
.
class MainActivity: AppCompatActivity() { | |
@Inject | |
lateinit var mainPresenter: MainPresenter | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
// inject mainPresenter here | |
mainPresenter.getData() | |
} | |
} |
Now the fields are annotated then we can proceed to inject dependencies from our cached into entryPointClass
using Field.set(entryPointClass, dependencyInstance)
.
object Injector { | |
fun <T : InjectorModule, R : Any> inject(kClass: KClass<T>, entryPointClass: R) { | |
// get all provider methods | |
val methods = kClass.java.declaredMethods | |
.filter { method -> method.isAnnotationPresent(Provides::class.java) } | |
// construct module instance | |
val moduleInstance = kClass.java.newInstance() | |
// construct and cache dependencies | |
val dependencies: MutableMap<Class<*>, Any> = mutableMapOf() | |
methods.forEach { method -> | |
dependencies[method.returnType] = method.invoke(moduleInstance) | |
} | |
// Inject dependencies | |
entryPointClass.javaClass.fields.filter { field -> | |
field.isAnnotationPresent(Inject::class.java) | |
}.forEach { field -> | |
// field.type will return Class<*> | |
field.set(entryPointClass, dependencies[field.type]) | |
} | |
} | |
} |
Then we wire up the Injector into onCreate
method of MainActivity
.
class MainActivity: AppCompatActivity() { | |
@Inject | |
lateinit var mainPresenter: MainPresenter | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
Injector.inject(MainModule::class, this) | |
mainPresenter.getData() | |
} | |
} |
Voila ! There goes our simple Dependency Injection Library using Injector
and it’s custom annotations. But we’re not done yet, we will face another DI issue. Issues are what if MainPresenter
does have another dependency like this:
MainPresenterImpl.kt
Using method.invoke(instance)
will not work as we need to supply another dependencies into the method parameter that will turn out like this at MainModule
.
MainModule.kt
Which means, we are going to create logic in Injector
that are able to construct dependency instances that have another dependencies. It can be visualized as a tree of dependencies that are going to depend on one another. In that case, we will construct DI tree and all of dependency instances using DFS (sounds interesting). We are going to take up the challenge in the Part 2 of this DI Library series. For spoiler, you can checkout the solution at my Github here.
Thanks for reading. Stay tuned for my next article.
Full Source code: https://github.com/WendyYanto/kotlin-dependency-injection-lib
My Github : https://github.com/WendyYanto