I know that dependency injection is a complex topic and is known to scare the shit out of some developers. In this article, we will discuss the basics of dependency injection and will take a look at how it can be implemented in Android.
Today, we are going to discuss in-depth about dependency injection in Android and why you should be using it.
Table of contents
Open Table of contents
Definitions and advantages
Dependency injection is a technique where objects are created by an external entity or object. This means an object doesn’t have to create the instances of its dependent classes on his own but rather gets the dependencies from another object or static method.
This technique has many benefits here I’m just listing the most important ones:
- Reduces the boilerplate code
- Makes our code reusable and clean
- Makes it easy to replace our dependencies with fake implementations which make testing easier
- Helps us enable loose coupling
Now that we know about the basic concepts of dependency injection we will take a look at the different solutions and frameworks for dependency injection in Android.
Kodein
Kodein is a very simple and yet very useful dependency framework which is easy to configure and use. It’s easy to learn and provides some great benefits, with the most important being:
- It proposes a very simple and readable declarative DSL
- It handles dependency initialization order
- It easily binds classes and interfaces to their instance or provider
Declaring Bindings:
To make our dependencies accessible we need to make our class Kodein aware by implementing the KodeinAware interface.
class KodeinApplication: Application(), KodeinAware {
override val kodein = Kodein.lazy {
}
}
After that, we can define bindings using Kodeins dead simple DSL.
class KodeinApplication: Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this@KodeinApplication))
bind<Dice>() with singleton {
RandomDice()
}
}
}
Here we bind an example dice class using the singleton bind function.
Retrieval:
After that, we can start getting our dependency by making our activity KodeinAware. Then we just need to override the kodein variable and get our dependency by instance.
class MainActivity : AppCompatActivity(), KodeinAware{
override val kodein by closestKodein()
val dice: Dice by instance()
}
In this example, we use closestKodein which returns the Kodein of the closest parent layer.
Create modules:
Now let’s look at how we can split our bindings into modules.
val uiModule = Kodein.Module {
bind() from singleton { Controler(instance()) }
}
val businessModule = Kodein.Module {
bind<IService>() with singleton { BusinessService() }
}
Here we create two different modules with dependencies.
After that, we can import the modules we want to use in our Kodein container.
val kodein = Kodein {
import(uiModule)
import(businessModule)
}
Bind Functions:
Factory:
The factory bind function allows you to pass an argument of a certain type and return an instance of another type. A factory can take up to five arguments.
val kodein = Kodein {
bind<Dice>() with factory { sides: Int -> RandomDice(sides) }
}
Here we create a dice with the Int representing the number of sides.
Provider:
The provider function takes no argument and returns the bound type. It will be called every time you need an instance of the bound type.
val kodein = Kodein {
bind<Dice>() with provider { RandomDice(6) }
}
Here we create a dice with a fixed number of sides.
Koin
Koin is a lightweight but still powerful dependency injection framework for Kotlin developers which is purely written in Kotlin. Here are some of its benefits:
- Provides an easy to understand DSL
- Supports Android Architecture ViewModel
- Has built-in testing features using JUnit
Declaring Bindings in a module:
First, we need to define a module using the module keyword.After that, we can declare our binding using a binding function.
val appModule = module {
single<HelloRepository> { HelloRepositoryImpl() }
factory { MySimplePresenter(get()) }
}
Here we bind a repository using the singleton pattern to make sure it only gets instantiated once. After the first request, koin will just return the same instance over and over again. We also bind a presenter using the factory pattern which creates a new instance on each request.
Start Koin:
Before we can start retrieving our bindings we first need to start Koin in our application class.
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(appModule))
}
}
Also, don’t forget to set the name to your application class in your AndroidManifest file.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.dependencyinjection">
<application
android:name=".App"
</application>
</manifest>
Retrieval:
Now we can easily get our dependencies.
val firstPresenter: MySimplePresenter by inject()
Here we are using a lazy inject to get our presenter dependency. We can also directly inject our dependency using the get keyword.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val secondPresenter: MySimplePresenter = get()
}
Testing:
Koin provides test functionality which allows you to easily inject dependencies in your JUnit test. To use Koin in your Unit-test you just need to inherit from KoinTest.
class SimpleTest : KoinTest {
@Test
fun `check module`() {
checkModules(listOf(appModule))
}
val firstPresenter: MySimplePresenter by inject()
@Test
fun myTest() {
startKoin(listOf(appModule))
val secondPresenter: MySimplePresenter = get()
assertNotEquals(firstPresenter, secondPresenter)
stopKoin()
}
}
Here we inject two instances of a fake presenter and check if they are not the same instance. In the test above we use the checkModules function to check if all definitions are bounded.
Dagger:
Dagger is a fully static dependency injection framework which is based on annotations and helps us to manage our dependencies between classes. Here are some of its benefits:
- It makes your code very modular
- Makes it easier to unit test your code
- Helps you loosely couple your code
Annotations:
Dagger provides us with a few annotations we can use to define and inject our dependencies.
Inject:
This annotation can be used in two ways:
- Telling dagger to use this constructor to make an object of this type. This injection is recursive which means that if the constructor has parameters dagger will automatically try to inject them too.
- Used by a component to tell dagger that it wants this dependency.
Here is a simple example to make the statement clearer:
class Example @Inject constructor()
class MainClass{
@Inject
lateinit var example: Example
}
Here we create a class called Example which uses the @Inject on its constructor to tell dagger that it should use this constructor to inject dependencies of this type. After that we define the MainClass which ask Dagger to get an instance of Example and dagger tries to inject the dependency using the constructor.
Provides:
We use the provides annotation if there is no constructor we can inject from and when we can’t instantiate the dependency.
Marking a method with this annotation tells dagger that the method returns the datatype we want to inject. Now let’s look at an example to understand it better:
@Provides fun getContext(): Context {
return this.getContext()
}
In this example, we have a dummy function which returns a context. The @Provides annotation tells dagger where it can find the context.
The only problem that we can run into using this function is when we want to inject two different things with the same return-type. If something like this happens we need to use the Named annotation to rename our dependency manually.
@Named("ActivityContext")
@Provides fun getContext(): Context {
return this.getContext()
}
@Named("ApplicationContext")
@Provides fun getApplicationContexts(): Context{
return this.applicationContext
}
Here we have two functions with the same return type which we named using the Named annotation to let dagger know that they aren’t the same.
Note: Provide annotations can only be defined in a class which is annotated with @Module.
Module:
Modules tell dagger how to provide dependencies from the dependency graph. These are normally high-level dependencies that you haven’t already provided to the graph using @Inject.
Modules are defined as classes with an @Module annotation.
@Module
class AppModule{
...
}
This example shows how you can define a basic module using the @Module annotation on your class.
@Module
class AppModule(val activity: Activity){
@Named("ActivityContext")
@Provides fun getContext(): Context {
return activity.baseContext
}
@Named("ApplicationContext")
@Provides fun getApplicationContexts(): Context{
return activity.applicationContext
}
}
Here we actually define our provide function from above in our module.
Note: If you want more information about dagger I can highly recommend Hari Vignesh Jayapalan introduction to dagger2 posts where he goes from the basics to more advanced dagger topics in just a few posts.
Conclusion
Now you should know how the dependency injection works and why it’s important to use it. We also looked at two big dependency injection frameworks and why they are a great choice for dependency injection in Kotlin.
If you have found this useful, please consider recommending and sharing it with other fellow developers.
If you have any questions or critics, you can reach me in the comment section.