Dependency Injection With Koin

In this tutorial, you’ll get to know Koin, one of the most popular new frameworks for dependency injection.

4/5 1 Rating · Leave a Rating

Version

  • Kotlin 1.3, Android 4.1, Android Studio 3

Dependency Injection (DI) is one of those “new” concepts that keeps showing up in every blog post on the Internet. In fact, the idea is not new, but a notion that gets revisited from time to time. Mastering DI will allow you to handle large and complex applications in a more convenient way.

In this tutorial, you’ll get to know Koin, one of the most popular new frameworks for DI. You’ll start by learning the basics of DI and how your Android projects can take advantage of it. You’ll then apply it to a sample app that adopts Koin as a DI framework and illustrates its benefits.

DI: A “New” Old Friend

Believe it or not, many Android developers have been using DI since their very first applications. However, most new developers haven’t used any DI framework, such as Dagger 2, on their first projects. This sounds ridiculous, but it’s true. How? Follows a recap of the basics of DI.

In the illustration above, ClassA uses an object instantiated, or created, by itself, whereas ClassB just employs an object instance, regardless of where it comes from. So, it can be stated that DI means that a specific entity or class instance can obtain any dependency it needs from the outer world. In other words, the class  is not concerned how it gets a dependency — just how to use it.

That’s great, but how do you give a dependency to an object? There are two main options:

  • Pass dependencies through the object constructor.
  • Use a DI framework.

Using one or the other depends on the developer, who should based his decision according to the size and complexity of the application. For more information about DI, check out this helpful resource.

To DI or Not to DI?

So what are the pros and cons of using DI in your project? To help you with the answer, take a look at the SOLID principles of object-oriented programming, which are five principles that improve the reusability of code and reduce need to refactor any class. DI is directly related with 2 of these pillars, specifically Single Responsibility Principle and Dependency Inversion. Briefly explained

  • Single Responsibility Principle states that every class or module in a program is responsible for just a single piece of that program’s functionality.
  • Dependency Inversion states that high level modules should not depend on low level modules; both should depend on abstractions.

DI supports these goals by decoupling the creation and the usage of an object. Thus, it allows you to replace dependencies without changing the class that uses them and also reduces the risk of modifying a class because one of its dependencies changed.

This makes DI a great option when an application is expected to grow considerably in size and/or complexity.

Using Kotlin to Simplify DI

So, why use Koin rather than one of the other DI frameworks? The answer: Koin is more concise and straightforward than the others.

Take the popular Dagger 2 as an example. To use Dagger 2, you first need to become familiar with concepts like module and component and annotations such as @Inject. While the steep learning curve for this framework eventually pays off, to get the most out of it, you still have to learn some advanced concepts such as scope and subcomponents.

In contrast, Koin allows you to simply declare modules, which include potential dependencies, to be used in the project and directly inject them in the class of interest.

Koin Basics

According to the official documentation, you can start using Koin in three simple stages:

  1. Declare a module: Defines those entities which will be injected at some point in the app.
    val applicationModule = module {
      single { AppRepository }
    }
  2. Start Koin: A single line, startKoin(this, listOf(applicationModule)), allows you to launch the DI process and indicate which modules will be available when needed, in this case, only applicationModule.
    class BaseApplication : Application() {
      override fun onCreate() {
        super.onCreate()
        startKoin(this, listOf(applicationModule))
      }
    }
  3. Perform an injection:
    In consonance with Kotlin features, Koin allows to perform lazy injections in a very convenient way.
    class FeatureActivity : AppCompatActivity() {
      private val appRepository: AppRepository by inject()
      ...
    }

One limitation when using Koin is that you can only inject dependencies in Activity classes out of the box. In order to inject dependencies in other class types, you must do it through the corresponding constructors. To solve this problem, Koin allows classes to conform to the KoinComponent interface, so that injections are possible on non-Activity classes. You will see an example of this later on.

Getting Started

Now that the theory is clear, time to get to the action! From this section on, you’ll create an application named Mark me!. You’ll see how an application can include DI while following a proper architecture that advocates for the separation of concerns. You’ll use the popular model-view-presenter (MVP) as the architectural pattern in the presentation layer.

Begin by downloading the starter project using the Download Materials button at the top or bottom of the tutorial. The starter project contains the basic skeleton app and some assets.

The code is organized by functional modules, specifically splash, main and feature. Moreover, the application includes auxiliary packages such as di, for DI, repository, including database feature, model and utils.

Build and run the starter project.

As you can see, the application looks finished. However, before it can function, you still need to add all of the logic. If you tap on either the Attendance or Grading buttons, the application will report a crash due to the lack of implementation of certain functionalities.

Remember that, by default, Android Studio includes the following line in methods overridden from interfaces:

TODO("not implemented") //To change body of created functions use File | Settings | File Templates.

This will cause a crash until this snippet gets replaced with a proper method implementation.

Building Mark Me!

In this section, you’ll incorporate Koin as the DI framework for the application skeleton provided in the starter project.

Mark me! is an app designed for teachers. It allows a teacher to register the attendance and grading for a class.

The starter AndroidManifest.xml file shows three Activity items:

  • SplashActivity includes the MAIN intent-filter.
  • MainActivity allows the user to navigate to two features, Attendance and Grading.
  • FeatureActivity actually implements the mentioned features.

The app skeleton includes the Student class defined in the Data.kt file in the model package. The project also contains a pair of adapters in the feature package. These implementations do not directly relate to the topic of this tutorial, but feel free to have a look at them and analyze their behavior.

Take some time to inspect the rest of the starter project and all the features included out-of-the-box, such as the resource files strings.xml, dimens.xml and styles.xml.

Adding Koin to the Project

First, you’ll add Koin to the app dependencies. Open the project build.gradle and add the following line in the ext block of the buildscript object:

koin_version = '1.0.2'

Then, refer to the build.gradle of the app module and include the next dependency in the corresponding section:

// Koin for Android
implementation "org.koin:koin-android:$koin_version"

Now, sync your project and you’ll be ready to start using Koin.

Defining Dependencies

Once you’ve added Koin to the project, you can start defining the dependencies that will be injected in your code when required.

If you review the project, you’ll see that, to finish Mark me!, you need to indicate whether the information will save in a database or the user preferences. Create a package di and a new file Modules.kt where you’ll define the entities to be provided.

Then, add the following snippet, taking care to import what the IDE suggests in each case.

val applicationModule = module(override = true) {
    factory<SplashContract.Presenter> { (view: SplashContract.View) -> SplashPresenter(view) }
    factory<MainContract.Presenter> { (view: MainContract.View) -> MainPresenter(view) }
    factory<FeatureContract.Presenter<Student>> { (view: FeatureContract.View<Student>) -> FeaturePresenter(view) }
    single<FeatureContract.Model<Student>> { AppRepository }
    single<SharedPreferences> { androidContext().getSharedPreferences("SharedPreferences", Context.MODE_PRIVATE) }
    single {
        Room.databaseBuilder(androidContext(),
                AppDatabase::class.java, "app-database").build()
    }
}

As you can see, the above code creates a new Koin module, which includes several important entities. Keep in mind:

  1. The module is marked as override, which means that its content will override any other definition within the application.
  2. A factory is a definition that will give you a new instance each time you ask for this object type. In other words, it represents a normal object. Any presenter involved in the application will be injected in the view instance in this way.
  3. single depicts a singleton component such as an instance that is unique across the application. This is typically intended for repositories, databases, etc.
Note: Koin single and factory object declarations allow you to include a type in angle brackets and a lambda expression, which defines the way the object will be constructed. Due to SOLID principles, the indicated type is usually an interface that the object to inject has to implement. This makes this object easily exchangeable in the future. For example, in the first case the SplashPresenter needs to implement the SplashContract.Presenter and will use a SplashContract.View object as an argument constructor.

Starting Koin

Since the dependency module is already defined, you only need to declare its availability. Open BaseApplication.kt and include the following snippet, making sure the imports are there as well:

class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin(this, listOf(applicationModule))
    }
}

As you can see, the applicationModule now includes the list of dependency modules for Koin. Next, make this class visible in the Manifest as the application class:

  ...	 	 
  <application 
    android:name=".BaseApplication"	 	 
    ... 	 	 

Injecting Objects

As previously stated, DI helps make code easier to reuse and test. Taking into account that you are using an MVP architecture implementation, the aim is to make any presenter in the app as decoupled as possible. In other words, presenters will receive but not instantiate other classes.

Start by modifying how the SplashActivity instantiates the presenter.

Initially, you can see that it is created in a lazy way on this line:

private val splashPresenter : SplashContract.Presenter by lazy { SplashPresenter(this) }

However, when you use DI, the line will look like this:

private val splashPresenter: SplashContract.Presenter by inject { parametersOf(this) }

Now, splashPresenter gets lazily injected when needed. The expression parametersOf() is part of the Koin library and allows you to indicate input arguments for the object constructor. If you recall in the Modules.kt file, you defined a factory to return a new SplashContract.Presenter when a SplashContract.View is given. In this case, the SplashActivity is the SplashContract.View and is passed into the factory through parametersOf().

Similar to the previous module, you also need to update how MainActivity obtains its presenter. The new form should be:

private val mainPresenter: MainContract.Presenter by inject { parametersOf(this) }

Now the feature package content needs some updating. In FeatureActivity, change the presenter invocation by replacing:

private val featurePresenter: FeatureContract.Presenter<Student> by lazy { FeaturePresenter(this) }

With:

private val featurePresenter: FeatureContract.Presenter by inject { parametersOf(this) }

Now, complete the method onResume by adding:

// Load persisted data if any
featurePresenter.loadPersistedData(data = classList, featureType = featureType)

This tells the presenter to load any persistent data available. If you happen to run the project right now, you will get a crash on this precise line since loadPersistedData doesn’t have implementation yet. Don’t worry, you’re going to fix this.

A bit further in the code, replace a pair of TODOs as follows:

override fun showToastMessage(msg: String) {
  toast(msg)   // Anko utility for Toast messages
}

override fun onPersistedDataLoaded(data: List<Student>) {
  (rvItems?.adapter as? RwAdapter<Student>)?.updateData(data)
}

When the user stores any data, a message will appear thanks to showToastMessage. You’ll use onPersistedDataLoaded to publish the fetched data in a list. Obviously, you still need to define updateData.

Open the RwAdapter interface and paste the following abstract method there:

fun updateData(data: List<T>)

You’ll see that both FeatureGradingAdapter and FeatureAttendanceAdapter demand an implementation for this method. For FeatureGradingAdapter, add the following:

override fun updateData(data: List<Student>) {
        data.forEachIndexed { index, student ->
            dataList?.first { student.name == it.name }?.grade = student.grade
            notifyItemChanged(index)
        }
    }

The proposal for FeatureAttendanceAdapter is:

 override fun updateData(data: List<Student>) {
        data.forEach { student ->
            dataList?.first { student.name == it.name }?.attendance = student.attendance
        }
        notifyDataSetChanged()
    }

As you can see, the implementations are pretty similar but not exactly the same. In the first implementation, the changes to the attendance list are individually notified to the adapter, while in the second implementation, the whole grading list is changed as a group at the end of the loop. The only reason for this difference is to show you two possible approaches.

Finally, in FeaturePresenter, modify how the repository instantiates the so that it looks like this:

private val repository: FeatureContract.Model<Student> by inject()

Don't forget to turn the class into a KoinComponent.

class FeaturePresenter(private var view: FeatureContract.View<Student>?)
    : FeatureContract.Presenter<Student>, KoinComponent {

Recall that Koin cannot inject non-Activity objects out of the box. In this case, since FeaturePresenter is not an instance of Activity, you must add the KoinComponent interface to the class.

Now, it’s time to provide a proper definition for loadPersistedData, which will end up looking like this:

override fun loadPersistedData(data: List<Student>, featureType: ClassSection) {
        when (featureType) {
            ClassSection.ATTENDANCE -> repository.fetchFromPrefs(data)
            ClassSection.GRADING -> repository.fetchFromDb(data = data,
                    callback = { loadedData ->
                        view?.onPersistedDataLoaded(loadedData)
                    })
        }
    }

Again, don't be upset if you run your code now and you get a crash since you haven’t provided any definition for fetchFromDb yet.

Finishing Up!

Last, you’ll work on the repository module — specifically, the singleton class AppRepository. When you open it up, you will see a few TODOs to get rid off. Time to tackle them!

Start by adding these two constants at the beginning of the file, before the class definition, so that they are available when needed:

private const val MSG_DATA_SAVED_TO_DB = "Data saved to DB"
private const val MSG_DATA_SAVED_TO_PREFS = "Data saved to prefs"

Then, proceed by replacing the first method in AppRepository, turning it into this:

override fun add2Db(data: List<Student>, callback: (String) -> Unit) {
        doAsync {
            database.userDao().insertStudentList(data)
            uiThread {
                callback(MSG_DATA_SAVED_TO_DB)
            }
        }
    }

The method asynchronously inserts a list in the database and notifies it through a message in a lambda callback.

Once you add the above snippet, you’ll get an unresolved reference error because database was never injected into AppRepository. This entity refers to the Room implementation set up in the project. This entity was also defined within the app dependencies.

Add the next line right after the class header,

private val database: AppDatabase by inject()

and make sure AppRepository implements the KoinComponent interface.

object AppRepository : FeatureContract.Model<Student>, KoinComponent {

The next method to fill in allows you to save data in the app SharedPreferences. Again, it uses Anko to perform the task in the background by using doAsync and to notify it in the main thread by using uiThread:

override fun add2Prefs(data: List<Student>, callback: (String) -> Unit) {
        doAsync {
            data.forEach {
                with(sharedPreferences.edit()) {
                    val jsonString = Gson().toJson(it)
                    putString(it.name, jsonString).commit()
                }
            }
            uiThread {
                callback(MSG_DATA_SAVED_TO_PREFS)
            }
        }
    }

The last two methods implement fetching data from the database and the preferences, respectively.

override fun fetchFromDb(data: List<Student>, callback: (List<Student>) -> Unit) {
        doAsync {
            val list = database.userDao().loadAllStudents()
            uiThread {
                callback(list)
            }
        }
    }

    override fun fetchFromPrefs(data: List<Student>): List<Student> {
        data.forEach {
            val item: Student? = Gson().fromJson(sharedPreferences.getString(it.name, ""), Student::class.java)
            item?.let { persItem ->
                it.attendance = persItem.attendance
                it.grade = persItem.grade
            }
        }

        return data
    }

The only difference between the two methods above is that the database queries are performed in a worker thread, not the main one.

And with that, you have finished implementing Mark me! Congratulations!

App Performance Analysis

Once completed, you should have the app up and running as in the following Mark me! demo video:

When started, the splash screen appears for just a few seconds, and then it jumps directly to MainActivity. From this screen the user can navigate to either of the available features: Attendance or Grading. Both of them provide a list that shows certain information about a class of students.

You can gracefully scroll along these lists and check/uncheck in one or several student registers. Tapping on the corresponding buttons saves the selections. The navigation is handled by the Toolbar and Back Arrow events.

Testing: Insert Koin

To properly asses a DI framework, you need to know how well it behaves when it comes to a unit test. Good news! Koin perfectly addresses this need.

First, you’ll add some dependencies to the app module build.gradle:

dependencies {
    ...
    // Koin testing tools
    testImplementation "org.koin:koin-test:$koin_version"
    testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
}

The first library brings in Koin DSL, version 1.0.2 in this case, for testing, whereas the second adds extra features to Mockito so it can work with Kotlin.

Note: If you've never heard of Mockito, it's a mocking framework for unit tests in Java. If you're interested in adding Mockito to your project, you can read about Android Unit Testing with Mockito on our site.

Generally speaking, unit tests make sense on non-Android layers since you want to check your logic, but not the framework itself. Since this project uses MVP as the architecture pattern, presenters should be the target.

In this example, you’re only going to check that a method is called when a certain condition happens, which will prove the compatibility and convenience of using Koin for testing.

Checking the Approach

According to the above, create a file FeaturePresenterTest.kt in the test folder.

Then, add the following snippet:

class FeaturePresenterTest : KoinTest {

    private val view: FeatureContract.View<Student> = mock()
    private val repository: FeatureContract.Model<Student> by inject()
    private val presenter: FeatureContract.Presenter<Student> by inject { parametersOf(view) }

    @Before
    fun before() {
        startKoin(listOf(applicationModule))
        declareMock<FeatureContract.Model<Student>>()
    }

    @After
    fun after() {
        stopKoin()
    }

    @Test
    fun `check that onSave2DbClick invokes a repository callback`() {
        val studentList = listOf(
                Student(0, "Pablo", true, 8),
                Student(1, "Irene", false, 10))
        val dummyCallback = argumentCaptor<(String) -> Unit>()

        presenter.onSave2DbClick(studentList)
        Mockito.verify(repository).add2Db(data = eq(studentList), callback = dummyCallback.capture())
    }
}

While the code is fairly straightforward, here are some things to keep in mind:

  1. The class must implement the KoinTest interface.
  2. All the elements used must be instantiated or injected, even if you are not using them directly, like the view. Since the Android OS does not allow you to invoke Activity objects, the view has to be directly mocked.
  3. Koin needs to start normally so that injections take place, including all of the dependency modules.
  4. All objects not being tested have to be mocked. In Koin, you can use declareMock for those injected objects.
  5. It’s good practice to use descriptive names for your methods. Remember that you can use `` to enclose a name with whitespaces.
  6. argumentCaptor allows you to grab a value or variable and use it later with any Mockito expression. This is required when using Koin.
  7. Since you’re checking the presenter, invoke one of its methods with the appropriate arguments.
  8. This line is where the evaluation actually happens. In this case, you verify that the add2Db method from the repository gets called. If you remember the FeaturePresenter class, this should be the case when invoking onSave2DbClick. The eq method is also mandatory due to limitations between Kotlin and Mockito.
  9. Always remember to stop Koin.

Run the test and make sure everything passes!

Where to Go From Here?

You can download the fully finished sample project using the Download Materials button at the top or bottom of the tutorial.

While there are several good references about DI, and Koin in particular, the best source may be the official documentation, which is rather concise and self-explanatory.

Articles like this one make starting to work with Koin much easier. This article is a more practical example, which may help you on your first attempts.

If you want to keep working with the sample app you’ve just finished, try replacing the presentation layer architecture with MVVM. This pattern is becoming very popular in the field, especially after the Google I/O 2017 conference, where it was incorporated into the Android Architecture Components (AAC). Koin includes a specific DSL classes to handle this new ViewModel stuff.

I hope you enjoyed this tutorial about Koin. If you have any questions or comments, please join the forum discussion below!

Average Rating

4/5

Add a rating for this content

1 rating

Contributors

Comments