Dependency Injection With Koin

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

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.