Applying Koin
With all that in mind, let’s look at the DiceKoin
sample module in the Sampler
project, to see how Koin works and what problems it solves.
The Dependency
As with most things, Koin comes from a library. Actually, Koin is made up of several libraries, to handle different scenarios. For example, you can use Koin for non-Android projects using Kotlin.
In our case, we are using koin-androidx-viewmodel
:
implementation "io.insert-koin:koin-androidx-viewmodel:2.2.3"
This not only pulls in Koin and Koin’s support for Android, but it offers specific support for the Jetpack edition of ViewModel
, as we will see.
PassphraseRepository
PassphraseRepository
is now a class
, not an object
as it was before. And, it gets a Context
and a SecureRandom
in its constructor. This allows us to avoid the Context
parameter on functions like generate()
and the locally-initialized SecureRandom
instance:
package com.commonsware.jetpack.diceware
import android.content.Context
import android.net.Uri
import android.util.LruCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.security.SecureRandom
import java.util.*
val ASSET_URI: Uri =
Uri.parse("file:///android_asset/eff_short_wordlist_2_0.txt")
private const val ASSET_FILENAME = "eff_short_wordlist_2_0.txt"
class PassphraseRepository(
private val context: Context,
private val random: SecureRandom
) {
private val wordsCache = LruCache<Uri, List<String>>(4)
suspend fun generate(wordsDoc: Uri, count: Int): List<String> {
var words: List<String>?
synchronized(wordsCache) {
words = wordsCache.get(wordsDoc)
}
return words?.let { rollDemBones(it, count) }
?: loadAndGenerate(wordsDoc, count)
}
private suspend fun loadAndGenerate(wordsDoc: Uri, count: Int): List<String> =
withContext(Dispatchers.IO) {
val inputStream: InputStream? = if (wordsDoc == ASSET_URI) {
context.assets.open(ASSET_FILENAME)
} else {
context.contentResolver.openInputStream(wordsDoc)
}
inputStream?.use {
val words = it.readLines()
.map { line -> line.split("\t") }
.filter { pieces -> pieces.size == 2 }
.map { pieces -> pieces[1] }
synchronized(wordsCache) {
wordsCache.put(wordsDoc, words)
}
rollDemBones(words, count)
} ?: throw IllegalStateException("could not open $wordsDoc")
}
private fun rollDemBones(words: List<String>, wordCount: Int) =
List(wordCount) {
words[random.nextInt(words.size)]
}
private fun InputStream.readLines(): List<String> {
val result = ArrayList<String>()
BufferedReader(InputStreamReader(this)).forEachLine { result.add(it); }
return result
}
}
Otherwise, PassphraseRepository
is the same as it was, in terms of API and functionality.
MainMotor
MainMotor
has a couple of changes as well:
- It gets a
PassphraseRepository
via its constructor, rather than referencing the formerobject
form of the repository - It no longer needs to pass a
Context
in its calls to the repository, so it can be a regularViewModel
instead of anAndroidViewModel
package com.commonsware.jetpack.diceware
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private const val DEFAULT_WORD_COUNT = 6
class MainMotor(private val repo: PassphraseRepository) : ViewModel() {
private val _results = MutableLiveData<MainViewState>()
val results: LiveData<MainViewState> = _results
private var wordsDoc = ASSET_URI
init {
generatePassphrase(DEFAULT_WORD_COUNT)
}
fun generatePassphrase() {
generatePassphrase(
(results.value as? MainViewState.Content)?.wordCount ?: DEFAULT_WORD_COUNT
)
}
fun generatePassphrase(wordCount: Int) {
_results.value = MainViewState.Loading
viewModelScope.launch(Dispatchers.Main) {
_results.value = try {
val randomWords = repo.generate(wordsDoc, wordCount)
MainViewState.Content(randomWords.joinToString(" "), wordCount)
} catch (t: Throwable) {
MainViewState.Error(t)
}
}
}
fun generatePassphrase(wordsDoc: Uri) {
this.wordsDoc = wordsDoc
generatePassphrase()
}
}
Otherwise, it too is unchanged from before.
KoinApp
At some point, though, something needs to be creating an instance of PassphraseRepository
. MainMotor
is not doing that — it is expecting something else to create the instance and supply it to the MainMotor
constructor. Similarly, something needs to be creating that SecureRandom
instance to pass to PassphraseRepository
, whenever somebody gets around to making that repository instance.
The “something” is Koin.
Compared with the Diceware
sample, DiceKoin
has one new class: KoinApp
:
package com.commonsware.jetpack.diceware
import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
import java.security.SecureRandom
private val MODULE = module {
single { SecureRandom() }
single { PassphraseRepository(androidContext(), get()) }
viewModel { MainMotor(get()) }
}
class KoinApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@KoinApp)
modules(MODULE)
}
}
}
Subclass of Application
We have seen the Application
object before. It is a process-wide singleton instance that we can access at various points, such as calling getApplication()
on an Activity
or AndroidViewModel
. By default, it is an instance of android.app.Application
.
However, you can create your own subclass of Application
and use it instead.
The big reason to do this is to get control every time your process is forked. Application
has an onCreate()
function, much like how an Activity
does. onCreate()
on an Activity
is called when that Activity
is created — similarly, onCreate()
on an Application
is called when that Application
is created. Since the Application
singleton is created when your process starts, your code in onCreate()
will get called when your process starts.
As a result, we tend to use a custom Application
subclass for cases where we need to do some process-wide initialization. Setting up dependency inversion, whether using Koin or something else, is usually done in a custom Application
subclass.
Referenced in the Manifest
To tell Android to create an instance of your Application
subclass when your process starts, you need to add an android:name
attribute to the <application>
element itself in the manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
package="com.commonsware.jetpack.diceware"
xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".KoinApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Previously, we had only used android:name
for things like <activity>
. However, android:name
fills the same role: identifying the Java or Kotlin class that Android should use for this component.
An <application>
element without android:name
defaults to using android.app.Application
. In our case, we are telling Android to use this KoinApp
class.
Defines a Module
Part of what KoinApp
does is define a Koin “module”. A module states what objects Koin can make available to parts of your app.
The simplest way to declare a Koin module is via the module()
top-level function. This takes a lambda expression, in which you have statements that set up each of the Koin-managed objects (or factories of objects, as we will see).
Two of those statements are calls to single()
:
single { SecureRandom() }
single { PassphraseRepository(androidContext(), get()) }
single()
in a module()
says “hey, Koin, when something asks for an object, here is one that you can use… but only have one of it for the entire app”. single()
takes a lambda expression that creates an instance of some object, and single()
knows what type of object that is. When something needs an object of that type, Koin can execute that lambda expression (if needed) and hand over that singleton instance.
The first single()
call sets up a Koin-managed singleton instance of SecureRandom
. The second single()
call sets up a Koin-managed singleton instance of PassphraseRepository
.
Our lambda expression for creating the PassphraseRepository
instance needs to provide a Context
and a SecureRandom
to the PassphraseRepository
constructor. For the Context
, we use androidContext()
, which says, “hey, Koin, you should have a Context
that we can use here!” — we will see where that Context
comes from shortly. For the SecureRandom
, we use get()
, which says “hey, Koin, search this module for a supplier of SecureRandom
objects, and use that here!”. In our case, we set up a SecureRandom
singleton on the preceding line. So, Koin will take that singleton SecureRandom
and pass it to PassphraseRepository
, when it is time to create our singleton instance of PassphraseRepository
.
The third statement in our module is not a single()
, but a viewModel()
:
viewModel { MainMotor(get()) }
Courtesy of our koin-androidx-viewmodel
, this ties into the Jetpack ViewModel
system. Our lambda expression needs to return a ViewModel
, and in this case it creates an instance of MainMotor
. MainMotor
takes a PassphraseRepository
instance as a constructor parameter, and our get()
call causes Koin to retrieve the PassphraseRepository
singleton, creating it if it does not already exist.
Note that viewModel
is not creating a singleton instance. Rather, it will defer to the Jetpack ViewModel
system to return the proper ViewModel
for the activity or fragment that might request one.
Starts Koin
The onCreate()
function in KoinApp
chains to the superclass implementation of onCreate()
, then calls startKoin()
:
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@KoinApp)
modules(MODULE)
}
}
As the name suggests, startKoin()
starts Koin. By doing that here in onCreate()
, Koin will be ready for use in the rest of our app. Like module()
, startKoin()
takes a lambda expression where we can have a series of statements to describe our Koin configuration. Here, we have three:
-
androidLogger()
, telling Koin that if it has any messages to log, use Logcat -
androidContext()
, telling Koin whatContext
to return fromandroidContext()
calls in our module definition — in this case, we provide theKoinApp
itself -
modules()
, where we can provide one or more modules that we want Koin to support
MainActivity
MainActivity
now needs to use Koin to get its MainMotor
. Previously, we used Jetpack’s viewModels()
delegate. Now, we use Koin’s viewModel()
delegate:
private val motor: MainMotor by viewModel()
The net effect is that the first time we try using motor
, viewModel()
will look in the Koin modules to see if it knows how to create an object of the desired type. In this case, we configured how to create a MainMotor
.
For other types of objects, we would use an inject()
property delegate instead of viewModel()
. For example, if we wanted to directly obtain the PassphraseRepository
in MainActivity
, we would inject()
it. viewModel()
, as the name suggests, has special support for Android’s ViewModel
system.
The Dependency Inversion Flow
Here’s how this works in practice, when the user taps our launcher icon.
First, onCreate()
of KoinApp
gets called. There, we set up our Koin module. However, none of those Koin-managed objects are actually created yet. For example, the PassphraseRepository
is not created right away, nor is any instance of MainMotor
.
Eventually, MainActivity
gets instantiated. When we initialize motor
using by viewModel()
, Koin sets up a property delegate to retrieve a MainMotor
when needed.
We then reference motor
in onCreate()
of MainActivity
as before. When we do that, Koin’s property delegate tries to retrieve an existing MainMotor
instance for this activity. That will fail, as this is the first time we are creating a MainMotor
. So Koin executes the lambda expression that we provided to viewModel()
, intending to return that object as our MainMotor
from the property delegate.
That lambda expression has a get()
call for a parameter that needs a PassphraseRepository
. So, Koin looks around the module and finds the single()
for PassphraseRepository
. Since we have not created an instance of that yet, Koin executes our lambda expression that we provided to single()
, intending to return that object as our PassphraseRepository
from our get()
call.
That lambda expression in turn has a get()
call that needs a SecureRandom
. Once again, Koin examines the module for a match and finds the SecureRandom
single()
. Since we have not needed this before, Koin executes that lambda expression, saving that SecureRandom
for future get()
or inject()
calls, and returns it from the get()
call. That in turn allows the PassphraseRepository
to be created and cached by Koin for future get()
or inject()
calls. And, that allows us to create the MainMotor
and use it as our ViewModel
.
If we rotate the screen and undergo a configuration change, our new MainActivity
instance will once again use Koin to get its MainMotor
. This time, Koin will return the previous MainMotor
instance, courtesy of Jetpack’s ViewModel
system.
Suppose the user presses BACK, but then immediately re-launches our app (e.g., the BACK click was by accident). Now, when MainActivity
asks Koin for a MainMotor
, since this is a completely new activity instance, we get a completely new MainMotor
instance. However, most likely, this is the same process as before, since the user had not been gone very long. So, we use the same PassphraseRepository
and SecureRandom
as before, since Koin caches them and reuses those instances, as instructed by the single()
rule.
What This Buys Us
In the end, that’s not really much additional code… but it is a bit more complex than what we had originally. And it may not be obvious what we gained by it, since we still wind up with all the same objects as before, just connected in a different fashion.
When it comes time to test MainMotor
or MainActivity
, though, that is where dependency inversion starts to become important. As noted before, we will want a controlled test instance of SecureRandom
, one with a stable seed so we get a stable set of random numbers.
When our instrumented tests run, our KoinApp
still gets instantiated and still configures Koin — that does not change just because we are testing. However, after that occurs, and before we test our classes, we can change the Koin configuration. In particular, we can replace the self-seeding SecureRandom
with a manually-seeded SecureRandom
, so our tests become predictable. That code resides solely in our tests. PassphraseRepository
, in particular, does not know about this change… because so long as it gets some SecureRandom
instance, PassphraseRepository
can do its work. So, rather than having to teach PassphraseRepository
different rules for creating a SecureRandom
instance (“in tests, create it this way, otherwise create it this other way”), PassphraseRepository
receives a SecureRandom
. It is our Koin configuration code — in KoinApp
and our tests — that determine which type of SecureRandom
we use.
We will explore this in greater detail in an upcoming chapter.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.