We’ve been developing the new open source Android library at Appodeal Stack, and we can’t add plenty of third-party libraries.
Dependency Injection is a helpful design pattern that is useful in everyday coding. I won’t discuss the pros and cons of it’s usage, there are tons of articles about this pattern.
The aim was to create useful tool for my teammates and me, and further I’ll show how I’ve achieved it. I was inspired by great Koin DI-framework I’ve been using in my pet-projects. Looking ahead I would say, the result of the DI-framework is near 90 lines of code. Go!
The aim.
As I’ve mentioned before, I like Koin-style for its simplicity and straight forwardness. So, in the end declaring DI module should look like:
module { | |
singleton<A> { AImpl() } | |
factory<B> { | |
BImpl( | |
a = get() | |
) | |
} | |
factoryWithParams<C> { (aParam) -> | |
C( | |
a = aParam as A, | |
b = get() | |
) | |
} | |
} | |
Where A, B and C are simple interfaces and classes:
interface A { | |
fun foo() | |
} | |
class AImpl: A { | |
override fun foo() { | |
print("A") | |
} | |
} | |
interface B { | |
fun bar() | |
} | |
class BImpl(private val a: A): B { | |
override fun bar() { | |
print("B") | |
} | |
} | |
class C( | |
val a: A, | |
val b: B, | |
) |
And to obtain their instances, I wanted to use these API:
val aInstance = get<A>() // or val aInstance: A = get() | |
val bInstance = get<B>() | |
val cInstance = get<C> { | |
params(bInstance) | |
} |
Types of instances
In the first step let’s describe types of necessary instances. It could be factories and singletons. In my cases factories could be with and without dynamic parameters. That’s why I have three generic types: Factory, ParamFactory and Singleton.
Let’s create sealed interface InstanceType and describe our Generic types.
sealed interface InstanceType<T> { | |
fun interface Factory<T> : InstanceType<T> { | |
fun build(): T | |
} | |
fun interface ParamFactory<T> : InstanceType<T> { | |
fun build(vararg params: Any): T | |
class Params { | |
var parameters: Array<out Any> = arrayOf() | |
private set | |
fun params(vararg parameters: Any) { | |
this.parameters = parameters | |
} | |
} | |
} | |
class Singleton<T>(private val factory: Factory<T>) : InstanceType<T> { | |
val instance: T by lazy { | |
factory.build() | |
} | |
} | |
} |
Factories invoke fun build(): T each time we need new instance of its class.
For Singletons we need the only one instance, so we use val instance : T. Also, using a lazy-delegate helps to avoid creating instance immediately. Kotlin guarantees lazy is thread-safe, so we don’t have to worry about synchronising.
Fun Interfaces (or Single Abstract Method) is the new Kotlin sugar. Next gist shows the difference.
1. Regular interface declaring and implementation:
interface RegularFoo { | |
fun bar() : String | |
} | |
val regularFoo = object : RegularFoo { | |
override fun bar(): String { | |
return "hello world" | |
} | |
} | |
regularFoo.bar() |
2. fun interface declaring and implementation:
fun interface SamFoo { | |
fun bar() : String | |
} | |
val samFoo = SamFoo { | |
"hello world" | |
} | |
samFoo.bar() |
As you can see functional interfaces can have the only one fun method() (’cause it’s a single abstract method 😀). Here you can find out more info about functional interfaces.
Factories storage
Next step is to hold all factories and singletons. It is a simple Map-holder.
@PublishedApi | |
internal object SimpleDiStorage { | |
val instances = mutableMapOf<KClass<*>, InstanceType<*>>() | |
inline fun <reified T : Any> addFactory(factory: InstanceType<T>) { | |
check(instances[T::class] == null) { | |
"Definition for ${T::class} already added." | |
} | |
instances[T::class] = factory | |
} | |
inline fun <reified T : Any> getInstance(noinline parameters: (Params.() -> Unit)? = null): T { | |
return when (val factory = instances[T::class]) { | |
is InstanceType.Singleton -> factory.instance as T | |
is InstanceType.Factory -> factory.build() as T | |
is InstanceType.ParamFactory -> { | |
val factoryParams = Params().apply(requireNotNull(parameters)).parameters | |
factory.build(*factoryParams) as T | |
} | |
null -> error("No factory provided for class: ${T::class.java}") | |
} | |
} | |
} |
Job Offers
Two points here:
- While adding a new factory I check if a factory for this class was already added, to reduce misunderstanding of multiple definition.
- The internal object SimpleDiStorage marked with @PublishedApi annotation. This object is presented in a separate module, but it uses inline function. To not make SimpleDiStorage public we mark it with @PunlishedApi and further we can use our functions not only in its module.
Obtaining instances
Let’s create higher-order function for it: just retrieve the instance from the storage.
inline fun <reified T : Any> get(noinline params: (Params.() -> Unit)? = null): T { | |
return SimpleDiStorage.getInstance(params) | |
} |
Module for factory and singleton definitions
As we have three types of factories, let’s create three functions for adding each type to the storage.
object SimpleDiScope { | |
inline fun <reified T : Any> factory(factory: InstanceType.Factory<T>) { | |
SimpleDiStorage.addFactory(factory) | |
} | |
inline fun <reified T : Any> factoryWithParams(factory: InstanceType.ParamFactory<T>) { | |
SimpleDiStorage.addFactory(factory) | |
} | |
inline fun <reified T : Any> singleton(factory: InstanceType.Factory<T>) { | |
SimpleDiStorage.addFactory(InstanceType.Singleton<T>(factory)) | |
} | |
} |
But using SimpleDiScope directly is not beautiful, so let’s write new higher-order fun module to use it as a DSL.
fun module(scope: SimpleDiScope.() -> Unit) { | |
scope.invoke(SimpleDiScope) | |
} |
That’s it. Now we can declare factories and use Simple Dependency Injection. Of course, the functionality of Koin and Dagger is much broader, but this simple DI covers most of cases, costs 90 lines of code and doesn’t use third-party libraries.
This SimpleDi could be easily extended for your need, for instance, to create scoped instances or implement something like LRU cached instances to not hold a singleton if it is not used.
All in one is here on Github.
This article was originally published on proandroiddev.com on November 07, 2022