Koin - Simple Android DI

Dealing with dependency injection is one complex concept that I managed to understand before coming into the JVM space. I say this because Dagger-2 had complicated the concept for me when I came into android. Understanding how to arrange your code to accomplish what you wanted, took a lot of research and re-explaining. That said, Dagger-2 really did the job when you finally got how it worked.

I want to talk about another dependency injection framework that I recently adopted and how it has really simplified the process for me. Koin is a lightweight dependency injection framework, that uses Kotlin’s DSLs to lazily resolve your dependency graph at runtime.

If you are unfamiliar with the concept of dependency injection, you can read this article to understand what it is and how it is used. I am going to walk you through the key concepts of Koin, how to setup DI for Android and also Unit tests/Automated UI tests.

First get the library into your project. You will need to add the following to your module-level build.gradle.

// Add this to repositories if needed
repositories {
jcenter()
}

I will mention some comparisons with Dagger-2 if you are already familiar with it, and if you are not, don’t worry, just ignore those parts.

Koin is a DSL, using Kotlin’s DSLs, that describes your dependencies into modules and sub-modules. You describe your definitions with certain key functions that mean something in the Koin context.

I will explain the key functions later in this post, but first, you should know that after you have described your dependencies in Koin modules, you need to call the startKoin function passing Android Context and your list of modules. For example:

class SmapleApplication: Application() {

override fun onCreate() {
super.onCreate()

// get list of all modules
val moduleList = listOf(testModule)
// start koin with the module list
startKoin(this, moduleList)

// get log tree implementation
val tree: Timber.Tree = get()
Timber.plant(tree)
}
}

With Koin, setting up dependency injection is kind of a manual process, you essentially plug your services into the module graphs and those dependencies will later get resolved at runtime. Here are the key functions.

  • module { } - create a Koin Module or a submodule (inside a module)
  • factory { } - provide a factory bean definition
  • single { } - provide a bean definition
  • get() - resolve a component dependency
  • bind - additional Kotlin type binding for given bean definition
  • getProperty() - resolve a property

Module

The module definition is very similar to Dagger-2 @Module definitions, it serves as a container for a collection of services that should be provided at runtime. You put all the services that a module should provide in the module block. For example:

val dataModule = module {

single<SharedPreferenceManager>()

single<ContactManager>()

single { ContactServerManager(get()) }

Here we have a couple of things that I will like to address, the first thing is the module block after the = sign. This block is the container in which you will map out the services that this module provides, like in the code above, the data module provides a SharedPerferenceManager, a ContactManager, and a ContactServerManager. The other thing you will notice is that the module block is assigned as a value, this is because the Koin framework uses a list of modules to build the dependency graph, as you will see later in this post.

Factory

A factory serves the purpose of qualifying a dependency, it tells the Koin framework not to retain this instance but rather, create a new instance of this service anywhere the service is required. This can be done in a couple of ways:

interface SomeInterface {
fun getSimpleDate(): String
}

class SomeClass(val date: Date): SomeInterface {

override fun getSimpleDate(): String {
return SimpleDateFormat.getDateInstance().format(date)
}
}

val testModule = module {

// create one instance to be used
single { Date() }

// then you can automatically resolve SomeClass
factory<SomeClass>()

// or you can use a factory block to compose the service
factory {
val currentDate = Date()

return@factory SomeClass(currentDate)
}

First, ignore the single block, for now, we will discuss that next, and just focus on the factory areas.

There are a couple of things here to take note of, the first is the version of the factory function that just takes the generic parameter, that is an experimental feature that simplifies the code-block version. In that version, Koin will resolve all the dependencies of SomeClass, so that can be used for situations where you have already specified required dependencies for SomeClass in one of your Koin modules.

Koin also allows you to manually build the dependency, this can be used for cases where you want to build a dependency based on configurations or other criteria. This is what the code-block version of the factory does, you see first I create a new date and then I pass it as a constructor argument to create the SomeClass instance, which I return.

You also have situations where you want to provide an implementation for an interface, you can also do that using the automatic resolve with factoryBy, or you can manually build the dependency and specify the interface as the generic type parameter.

Single

The single definition does the exact opposite of the what factory does. It tells Koin to retain the instance and provide that single instance wherever needed. This can be done in some ways similar to the factory defined above. It is similar to Dagger-2 @Singleton annotation.

val testModule = module {

// create one instance to be used
single { Date() }

// then you can automatically resolve SomeClass
single<SomeClass>()

// or you can use a 'single' block to compose the service
single {
val currentDate = Date()

return@single SomeClass(currentDate)
}

Similar to the factory above, there are a couple of ways to define a single bean. Both the experimental features that just take type parameters and the one that you create and return within your lambda block.

Get

The get function is a generic function that is used to resolve dependencies where needed. You use it to resolve any particular dependency that you need and it could be used as follows.

val testModule = module {

// create one instance to be used
single { Date() }


factory<SomeInterface> {
// resolve dependency within module definition
val currentDate: Date = get() // or get<Date>()

return@factory SomeClass(currentDate)
}
}

class SomeActivity: AppCompatActivity() {

private lateinit var someService: SomeInterface

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// .. other setup

Bind

The bind is an infix function helps resolve additional types, what this means is, you create one bean definition and use the bind to resolve it as an additional type, for example:

interface Flammable {
fun getFlames(): String
}

interface SomeInterface: Flammable {
fun getSimpleDate(): String
}

class SomeClass(val date: Date): SomeInterface {

override fun getSimpleDate(): String {
return SimpleDateFormat.getDateInstance().format(date)
}

override fun getFlames(): String {
return "Look to your sins," +
" the night is dark and" +
" full of terrors"
}
}

val testModule = module {

// create one instance to be used
single { Date() }

// will provide dependency for SomeClass
// and SomeInterface and Flamable
single { SomeClass(get()) } bind SomeInterface::class bind Flammable::class
}

class SomeActivity: AppCompatActivity() {

private lateinit var someService: SomeInterface
private lateinit var someClass: SomeClass

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// .. other setup

// will resolve a dependency
// based on single bean defined above
someService = get()
// will resolve the same dependency
// based on single bean defined above
someClass = get()

}

//.... other stuff

}

The one thing to note here is that the single bean definition for SomeClass also binds to interfaces SomeInterface and Flammable, meaning with that single definition, you can provide services for the corresponding interfaces wherever required.

As you see when I try to resolve SomeInterface and SomeClass in the activity, the same definition is used to resolve the two. That’s the feature of the bind function.

Note: This does not only apply to single bean definitions it can also be used for factory definitions but in that case, a new instance will be created each time either of the types (SomeClass, SomeInterface or Flammable) is required.

Read or Write Properties

The dependency container can contain configuration properties or values that can be read and set at runtime. This is done using the getProperty or setProperty functions. These properties are in a key-value pair container, you read and write using a key, and specifying the value to set, or the type to read. for example:

    // In your Application class
val moudles = listOf(testModule)
startKoin(moudles)


//----------------------------

A Little More (And Some Advanced Stuff)

There are some other things within the Koin framework, some are advanced some are just other aspects of Koin you should take note of.

Scope

Koin’s scope allows you to define local scoped-dependencies that are short-lived, like a Presenter, View Adapter, or other service components that are meant to be short-lived within a defined scope, like an Activity’s scope. This is similar to Dagger-2 @Scope annotation.

With the scope definition, you must specify the scope_id which is just a unique string representing the scope definition. The specified scope_id can then later be used as a reference, in creating, requesting or closing a scope. Here is what it looks like:

// define scope in your module
module {
scope("scope_id") { Presenter() }
}

KoinComponent

The Koin component serves as the container context that allows you to interact with the Koin framework after you have defined your dependencies and started Koin (with the startKoin function), you get access to functions like get, inject, getKoin, viewModel, etc.

What I mean is, when you have started Koin and it has created its dependency container, to retrieve dependencies or interact with Koin, you would need to implement KoinComponent, or be a configuration-aware Android Component (i.e. Activity, Fragment, ContentProvider or Service). For example:

class SomeLazyService()
class SomeOtherService()


// In Activity no need for KoinComponent
class SomeActivity: AppCompatActivity() {

// evaluates dependency eagerly
val service: SomeOtherService = get()

The first thing to note is, SomeActivity doesn’t implement KoinComponent but it has access to the Koin container, this is because Koin already provides extension functions for Android configuration-aware components, so you can access the Koin context with an Activity or the like.

Another thing to take note of is the by inject function that is used. You might be wondering what differentiates it from the get function. The difference is, get is eagerly evaluated, that is, when Object creation gets to the field initialization phase, SomeOtherService will be retrieved from the dependency container, but SomeLazyService will not be resolved until the class tries to use it, because it is lazily evaluated, that is why the by keyword is used.

The last thing is the BroadcastReceiver. As you see it has to implement KoinComponent to interact with Koin.

ViewModel

Since google provided us with architecture components, it has been a ride with the MVVM architecture. Koin adds to such a ride by providing viewModel { } definition, and this enables you to define a viewModel dependency. Here is what it looks like:

class SomeOtherService()

val activityModule = module {
// ...
other dependencies

// block definition
viewModel { SomeViewModel(get()) }

// or
viewModel<SomeViewModel>()
}

class SomeViewModel(private val ss: SomeOtherService): ViewModel() {

fun doSomethingAwesome(param: String) {
//... does something awesome
}

}

class SomeActivity: AppCompatActivity() {

val activityViewmodel: SomeViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// does something with view model
}

}

Parameters for Definitions

When you try out Koin you will notice that module, factory, and single take optional parameters which are:

  • path: String - for a Koin module or a sub-module (inside a module)
  • name: String - for a Koin factory or single bean definition
  • createOnStart: Boolean - for a Koin single or module definition
  • override: Boolean - for all definitions

Path

The path parameter is for the module and sub-module definitions, it serves as the location reference for particular dependencies or sub-module segments. The path could be used to retrieve defined dependencies. Here is what it looks like:

val sampleModule = module("org.sample") { ... }

// is equivalent to
val sampleModule = module {
module("org") {
module("sample") {
// ...
}
}
}

Note that the root module has path is serves as the empty string, then the reset represent the nested modules. And it can be used to retrieve dependencies like so:

class SampleService()
class SampleSecondService()
class SampleThirdService()

One thing to notice is that, when trying to retrieve dependencies, the module paths are separated by dots, and the module paths come before the ClassName.

Here is how to look at this, look at the modules and sub-modules as java packages, and the single or factory dependency definitions as java classes inside java packages.

Since you are most likely familiar with Java packages and java files, you know that packages are just nested folders with java files, and when you want to reference some other java class you would use the package path and the ClassName (e.g. import com.your.domain.ClassName).

This same analogy goes for Koin modules and bean definitions, as you can see in the example above, to reference the SampleSecondService in the staging module, I used the path starting from the root module up until the module containing the dependency followed by the class name.

Note: Instead of using the eager resolution, get, you could also use the lazy resolution with the inject function. For example:

val injectedService: SampleSecondService by inject("root.production.SampleSecondService")

Name

The name parameter is used to override the default name that Koin gives each bean definition.

By default, Koin uses the module path combined with the class name, i.e. first_module.second_module.ClassName. If there are no module paths, Koin defaults to just the beans Class Name. For example:

val mockModule = module {
single { SampleService() }
single(name = "sample_service_name") { SampleSecondService() }
}

In the example above the default name is used for the first definition, you can decide to not pass a name parameter to the get function, as that is what its default implementation. But for the second definition, you must use the same name specified during bean definition when trying to retrieve the dependency.

CreateOnStart

The createOnStart parameter is used to set eager creation of dependencies, and it can be only used on single and module definitions, this flag tells Koin to immediately create all single definitions when the startKoin function is called. Here is an example:

val mockModule = module {
// eager create a single service
single<SampleDatabase>(createOnStart = true)

The createOnStart flag can be very helpful when you have some service(s) that might take time to resolve at runtime, it tells Koin to resolve those dependencies immediately when started and thus it could reduce or prevent lags when the user moves through the app.

Override

The override flag is used to override bean definitions, the flag marks any definition as overridable. This flag is very useful when you want to setup test doubles for your dependencies. So you can mark your test modules with the override flag and then it will override whatever existing services you previously set up. Here is what it looks like:


val actualModule = module {
single<SampleService>()
single<SampleSecondService>()
single<SampleThirdService>()
}

With the definitions you see that I have already provided dependencies that will run in production, so to substitute them, I will need to provide definitions marked with ‘override = true’.

Think of it like this, when you have a java class with methods (member functions), to override the behavior of any member function you mark your overriding function with the @Override attribute. This is kinda what the override = true flag is doing, you are marking your definitions to override pre-existing definitions.

Unit Testing

Of course, you need to write tests, unit tests should have been the first thing I talked about but out of the convention, I put it last.

When writing unit tests, you could specify the modules at runtime, and call startKoin within your test functions. There are just two things you need to do first and then you can start testing right away.

First, add the test library as a dependency in your module level gradle file.

// add test library to your module gradle file
dependencies {
//... other dependencies

Second, make sure that your test class implements KoinTest interface.

class DependencyGraphTest: KoinTest {

@Test
fun `should resolve dependency with name attribute`() {
// declare module and sub-modules
val mockModule = module {
single(name = "service_name") { SampleService() }
}

startKoin(listOf(mockModule))

val service: SampleService = get("service_name")

assertNotNull(service)
}
}

With this example above, you see that the test class implements KoinTest, and that is what gives you access to functions like get, inject, startKoin, getKoin, etc. Another thing that you would notice is that I can call startKoin without passing an Android Context.

Another helpful thing is that Koin gives a method that helps check that your dependency graph is well set up, so it will try and resolve all dependencies one by one in your graph.

@Test
fun checkDependencyGraph() {

// override any previously defined dependencies
val mockApplication = module(override = true) {

single { mock(Application::class.java) }
single { mock(Context::class.java) }
}


// get all modules
val moduleList = appModules + activityModules + mockApplication

checkModules(moduleList)
}

Note that I am overriding the Application and Context classes because, these classes are not available in a unit test, so you have to override them with mock implementations. And you have to do this for any component that requires an Android Device to be set up.

Conclusion:

That is it. You could find my sample project on the GitHub.

A software developer and a mathematical enthusiast. For mentorship find me on code mentor https://www.codementor.io/@ebiigweze

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store