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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dependency Injection With Koin
25 mins
- DI: A “New” Old Friend
- To DI or Not to DI?
- Using Koin to Simplify DI
- Koin Basics
- Getting Started
- Building Mark Me!
- Adding Koin to the Project
- Defining Dependencies
- Starting Koin
- Injecting Objects
- Finishing Up!
- App Performance Analysis
- Testing: Insert Koin
- Checking the Approach
- Where to Go From Here?
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 MAINintent-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:
- The
module
is marked asoverride
, which means that its content will override any other definition within the application. - 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. -
single
depicts a singleton component such as an instance that is unique across the application. This is typically intended for repositories, databases, etc.
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 TODO
s 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.