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:

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:

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()
You can learn more about property delegates in the "Property Delegates" chapter of Elements of Kotlin!

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.