Blog Infos
Author
Published
Topics
,
Published

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()
)
}
}
view raw DiStyle hosted with ❤ by GitHub

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,
)
view raw gistfile1.txt hosted with ❤ by GitHub

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)
}
view raw gistfile1.txt hosted with ❤ by GitHub
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()
}
}
}
view raw InstanceType hosted with ❤ by GitHub

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()
view raw RegularFoo hosted with ❤ by GitHub

2. fun interface declaring and implementation:

fun interface SamFoo {
fun bar() : String
}
val samFoo = SamFoo {
"hello world"
}
samFoo.bar()
view raw FunInterface hosted with ❤ by GitHub

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}")
}
}
}
view raw SimpleDiStorage hosted with ❤ by GitHub

Job Offers

Job Offers


    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

,

At long last we have Kotlin first at Meta!

Android started to support Kotlin 5 years ago and became the first-choice language three years ago. But Meta just announced Kotlin as the preferred and default language for our Android code base only 3 months…
Watch Video

At long last we have Kotlin first at Meta!

Peng Jiang & Sergei Rybalkin
Software Engineer & Kotlin
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Serge ...
Software Engineer & ...
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Ser ...
Software Engineer & Kotli ...
Meta

Jobs

Two points here:

  1. While adding a new factory I check if a factory for this class was already added, to reduce misunderstanding of multiple definition.
  2. 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)
}
view raw gistfile1.txt hosted with ❤ by GitHub
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))
}
}
view raw SimpleDiScope hosted with ❤ by GitHub

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)
}
view raw module hosted with ❤ by GitHub

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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu