Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Investing in Koin - Minneapolis

Investing in Koin - Minneapolis

Thanks to its clean design, Koin is quickly gaining popularity as an inversion of control framework. Highlights include its excellent documentation, accessible api, and the pure Kotlin source.

In this talk, we’ll go over basic usage like factories, singles, and modules. We’ll talk about scoping and lazy-loading modules, and we’ll look at its integration with Android screens and ViewModels. Most importantly, we’ll see how we can use Koin to easily generate mocked dependencies for clean and hermetic tests.

Rick Busarow

October 05, 2019
Tweet

More Decks by Rick Busarow

Other Decks in Programming

Transcript

  1. @rbusarow What’s Koin? What’s Koin? You may have seen it

    mentioned in the Android social media. It’s getting a lot of attention…
  2. @rbusarow Koin is a 2018 MBP w/ 6-core i7 Easy

    to use (except the keyboard) More than fast enough for all but the most extreme use-cases
  3. Koin @rbusarow Dagger Dagger is a headless Linux server Dual

    processors, 1 TB ram Uses the operating system you learned in college and haven’t used since Pain to set up, but will handle anything you could ever throw at it
  4. @rbusarow Screen ViewModel UserId Repository Dao Room Database App Context

    Service Retrofit OkHttpClient Base Url From our screen, we’ll have one ViewModel It will require a UserId and Repository The Repository will need a Service and Dao to do its I/O.
  5. @rbusarow Normal Control Flow Before we talk about Inversion of

    Control, let’s review what this looks like in the old way.
  6. @rbusarow class UserRepository { internal val service: UserService = retrofit.create<UserService>()

    internal val dao: UserDao = database.userDao } Normal Control Flow Two normal dependencies. But in order to create them….
  7. @rbusarow class UserRepository { internal val okHttpClient: OkHttpClient = OkHttpClient()

    internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val database: Database = Database.init(APP_CONTEXT) internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Normal Control Flow We have to create three more hidden dependencies. • it’s expensive. • Boilerplate • Difficult to test • Locks us in to a real database and real api calls
  8. @rbusarow class UserRepository { internal val okHttpClient: OkHttpClient = OkHttpClient()

    internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val database: Database = Database.init(APP_CONTEXT) internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Normal Control Flow
  9. @rbusarow Service Location Dependency Injection Inversion of Control This frequently

    gets broken down into two categories: Service Location Dependency Injection
  10. @rbusarow Service Location Dependency Injection class UserViewModel { private val

    repository = Locator.createRepository() } Inversion of Control Service Location often uses a singleton referenced inside class to retrieve dependency may return new or singleton instances
  11. @rbusarow Service Location Dependency Injection class UserViewModel { private val

    repository = Locator.createRepository() } class UserViewModel(val repository: UserRepository) { lateinit var otherDependency : OtherDependency } Inversion of Control Dependency Injection Constructor injection Field injection
  12. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    } Field Injection When creating Screens, we can’t pass them constructor parameters. necessary for classes like Android Activities and Fragments have to use default constructors We have to assign it later via a setter.
  13. class UserScreen : Activity() { lateinit var viewModel: UserViewModel override

    fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } @rbusarow Field Injection Injection is then triggered by the Screen’s lifecycle. We call out to an Injector object and request it to set properties.
  14. class UserScreen : Activity() { lateinit var viewModel: UserViewModel override

    fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } @rbusarow Field Injection Note that it’s not returning any value. There are three problems with this field injection.
  15. class UserScreen : Activity() { lateinit var viewModel: UserViewModel override

    fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } @rbusarow Field Injection
  16. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } fun foo(userScreen: UserScreen) { userScreen.viewModel.repository.getUserById(...) } Field Injection public The viewModel is an implementation detail. Shouldn’t be part of the Screen’s API But it’s public. This may be more of a style issue than anything.
  17. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } fun foo(userScreen: UserScreen) { userScreen.viewModel = UserViewModel(...) } Field Injection public mutable It’s also mutable The whole idea of a ViewModel is that it’s a persistent representation of the screen’s state.
  18. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    val viewState = viewModel.viewState override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } Field Injection public mutable late init And it’s lateinit. This isn’t idiomatic. When something is lateinit, you have to be defensive when accessing it. It has a viral effect, similar to introducing a nullable property.
  19. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    val viewState = viewModel.viewState override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } Field Injection CRASH!! public mutable late init
  20. @rbusarow class UserScreen : Activity() { lateinit var viewModel: UserViewModel

    val viewState by lazy { viewModel.viewState } override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } Field Injection public mutable late init Anything accessing a lateinit could be wrapped in a lazy Lazy is slightly inefficient The longer the lazy chain, the better chance the lateinit will be accessed early Imagine it’s being accessed in a reactive stream. Intermittent crashes which might only happen on some phones in some conditions… So let’s see if we can make this illegal state impossible.
  21. @rbusarow Constructor Injection By far, the preferred way of injecting

    is via the constructor, so let’s look at what that actually means…
  22. @rbusarow class UserRepository( private val service: UserService, private val dao:

    UserDao ) { … } Constructor Injection So you have a Repository… takes parameters for the Service and Dao. How does this get initialized?
  23. @rbusarow class UserViewModel { val repository = UserRepository(service = TODO(),

    dao = TODO()) } class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection It’s a dependency of the ViewModel, which means it needs to be initialized when the ViewModel is. Using normal flow we’d hard-code it like this. But that means we need the dependencies…
  24. @rbusarow class UserViewModel { val client = OkHttpClient() val retrofit

    = Retrofit.Builder() .baseUrl("https://example.dev") .client(client) .build() val service = retrofit.create<UserService>() val database = Database.init(APP_CONTEXT) val dao = database.userDao val repository = UserRepository(service = service, dao = dao) } class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection We can create the dependencies manually, but this is pretty much what we had before. If what we need is the Service and Dao, then let’s just inject them.
  25. @rbusarow class UserViewModel { val client = OkHttpClient() val retrofit

    = Retrofit.Builder() .baseUrl("https://example.dev") .client(client) .build() val service = retrofit.create<UserService>() val database = Database.init(APP_CONTEXT) val dao = database.userDao val repository = UserRepository(service = service, dao = dao) } class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection
  26. @rbusarow class UserViewModel(service: UserService, dao: UserDao) { val repository =

    UserRepository(service = service, dao = dao) } class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection This is better. The dependency resolution is somewhere else. But really, we should be injecting the Repository itself…
  27. @rbusarow class UserViewModel( private val repository: UserRepository ) : ViewModel()

    class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection But now we have the same problem. How does the ViewModel get created?
  28. @rbusarow class UserScreen : Activity() { val viewModel = UserViewModel(repository

    = TODO()) } class UserViewModel( private val repository: UserRepository ) : ViewModel() class UserRepository( private val service: UserService, private val dao: UserDao ) { … } Constructor Injection In the Activity, we have the same problem. We cannot create the dependencies themselves. We need to inject them.
  29. @rbusarow class UserScreen : Activity() { lateinit var viewModel :

    UserViewModel override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } Field Injection… So here we’re back to the same field injection as before. But I’m making a point about DI here…
  30. @rbusarow class UserScreen : Activity() { lateinit var viewModel :

    UserViewModel override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } Field Injection… DI is for passing parameters - not creating them. At some point, we’ll reach the end of the road, then we’ll need to actually create dependencies.
  31. @rbusarow Service Location For that you always need a service

    locator. Let’s look at how the Service Locator solves this problem.
  32. @rbusarow class UserScreen : Activity() { private val viewModel by

    lazy { ViewModelProviders .of(this) .get(UserViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } Service Location What the lifecycle library does is to use a Service Locator, something like this. However that only helps with ViewModels requires the Activity to know about the ViewModel implementation gets much more complicated if there are dependencies to pass in
  33. @rbusarow class UserScreen : Activity() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } Service Location Instead, it would be nice if we could just access a multi-purpose Locator which handles everything.
  34. @rbusarow class UserScreen : Activity() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } object Locator { inline fun <reified T> get(): T = … } Service Location Instead, it would be nice if we could just access a multi-purpose Locator which handles everything.
  35. @rbusarow class UserScreen : Activity() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } object Locator { inline fun <reified T> get(): T = … } Service Location Because we’ve made this function inline, we can also make it a reified generic The compiler puts this logic into the call site at compile time, and removes this function So even though Kotlin still does type-erasure, we know this function’s type because the call site knows its type.
  36. @rbusarow class UserScreen : Activity() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } object Locator { inline fun <reified T> get(): T = when (T::class) { UserViewModel::class -> … else -> throw Exception("This class isn't in the graph yet") } } Service Location Instead, it would be nice if we could just access a multi-purpose Locator which handles everything.
  37. @rbusarow class UserScreen : Activity() { private val presenter by

    lazy { Locator.get<UserPresenter>() } private val viewModel by lazy { Locator.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } object Locator { inline fun <reified T> get(): T = when (T::class) { UserViewModel::class -> … UserPresenter::class -> … else -> throw Exception("This class isn't in the graph yet") } } Service Location Instead, it would be nice if we could just access a multi-purpose Locator which handles everything. But now we’re locked in to a single dependency graph…
  38. @rbusarow class UserScreen : Activity() { private val presenter by

    lazy { Locator.instance.get<UserPresenter>() } private val viewModel by lazy { Locator.instance.get<UserViewModel>() } override fun onCreate(savedInstanceState: Bundle?) { Injector.inject(this) super.onCreate(savedInstanceState) } } interface Locator { fun <T> get(): T = … companion object { lateinit var instance: Locator } } Service Location What if you could have a singleton Locator which is able to create anything in your graph, but can be swapped out for a different implementation when needed? Now let’s talk about Koin.
  39. @rbusarow KOIN Koin has a lot of overloaded terms, including

    Application and Context Koin is a normal class
  40. class Koin { val graph: Graph fun <T> inject(): Lazy<T>

    fun <T> get(): T fun <S, P> bind(s: S): S fun createScope(scopeId: ScopeID, qualifier: Qualifier): Scope } @rbusarow Koin & KoinApplication Koin is essentially the service locator. It holds the dependency graph, adds new bindings, and retrieves dependencies.
  41. class KoinApplication { val koin = Koin() fun modules(modules: List<Module>):

    KoinApplication fun logger(logger: Logger): KoinApplication fun unloadModules(modules: List<Module>) fun close() } @rbusarow Koin & KoinApplication The KoinApplication is a container for the Koin instance. KoinApplication exposes the Koin instance for direct access, but also handles the bulk operations like loading and unloading modules.
  42. class KoinApplication { val koin = Koin() fun modules(modules: List<Module>):

    KoinApplication fun logger(logger: Logger): KoinApplication fun unloadModules(modules: List<Module>) fun close() } @rbusarow Koin & KoinApplication Not an Android Application This is not related to an AndroidApplication.
  43. @rbusarow object GlobalContext { internal var app: KoinApplication? = null

    fun get(): KoinApplication fun getOrNull(): KoinApplication? fun start(koinApplication: KoinApplication) fun stop() = synchronized(this) { app?.close() app = null } } GlobalContext It’s an object which holds a single, nullable reference to a KoinApplication. This allows us to have a global scope, but we’re able to swap it out as we need to, and we can have multiple instances of KoinApplication. In case we want to use that as a scoping tool.
  44. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication These two factory functions just create a KoinApplication.
  45. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication We’ll talk a bit more about this soon. We’re able to tag a binding, so that it’s automatically created essentially on app launch. But we only want that to happen once on the Global context. So now it’s time to look at what we actually do with a KoinApplication factory.
  46. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication startKoin just additionally assigns the application to the global context.
  47. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication
  48. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication We’re able to tag a binding, so that it’s automatically created essentially on app launch. But we only want that to happen once on the Global context. So now it’s time to look at what we actually do with a KoinApplication factory.
  49. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() } } MyApplication.kt Here we are in MyApplication’s onCreate.
  50. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() startKoin { // route all Koin logs through android.util.Log androidLogger() } } } MyApplication.kt Now I can add startKoin { } to create a new Global KoinApplication instance. I’ll add `androidLogger()` to the lambda because that’s what all the docs say to do. For non-Android projects, there’s still a logger which just prints to System.out. This will route all Koin messages through Logcat using Debug, Info, and Error. But I don’t use that. One thing I love about libraries which don’t use reflection or code generation is the ability to modify their work.
  51. @rbusarow class AndroidLogger(level: Level = Level.INFO) : Logger(level) { override

    fun log(level: Level, msg: MESSAGE) { if (this.level <= level) { when (this.level) { Level.DEBUG -> Log.d(KOIN_TAG, msg) Level.INFO -> Log.i(KOIN_TAG, msg) Level.ERROR -> Log.e(KOIN_TAG, msg) } } } } AndroidLogger.kt This is the source for the AndroidLogger class. It’s pretty simple, and that Logger base class has a public constructor.
  52. @rbusarow class AndroidLogger(level: Level = Level.INFO) : Logger(level) { override

    fun log(level: Level, msg: MESSAGE) { if (this.level <= level) { when (this.level) { Level.DEBUG -> Log.d(KOIN_TAG, msg) Level.INFO -> Log.i(KOIN_TAG, msg) Level.ERROR -> Log.e(KOIN_TAG, msg) } } } } fun KoinApplication.androidLogger(level: Level = Level.INFO): KoinApplication { KoinApplication.logger = AndroidLogger(level) return this } AndroidLogger.kt This is the factory extension builder function which creates the logger and applies it to KoinApplication.
  53. @rbusarow class TimberLogger(level: Level = Level.INFO) : Logger(level) { override

    fun log(level: Level, msg: MESSAGE) { if (this.level <= level) { when (this.level) { Level.DEBUG -> Timber.tag(KOIN_TAG).d(msg) Level.INFO -> Timber.tag(KOIN_TAG).i(msg) Level.ERROR -> Timber.tag(KOIN_TAG).e(msg) } } } } fun KoinApplication.timberLogger(level: Level = Level.INFO): KoinApplication { KoinApplication.logger = TimberLogger(level) return this } TimberLogger.kt I just created my own. It’s identical but uses Timber. Now all logs will go through Timber like everything else.
  54. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() } } } MyApplication.kt Next I can add my Application context to the graph. After this, any dependencies which require an Android Context can use ApplicationContext.
  55. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) } } } MyApplication.kt Next I can add my Application context to the graph. After this, any dependencies which require an Android Context can use ApplicationContext.
  56. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } MyApplication.kt Next, I’ll add the modules. This adds all of my dependency definitions to the graph, so that they’re available for consumption. And that’s it for koinApplication and startKoin for the moment. Koin is initialized and ready to make dependencies. Now we get to talk about modules…
  57. @rbusarow Modules A module in Koin is similar to a

    module in Dagger. They represent a logical grouping of dependency definitions. Perhaps unlike Dagger, they’re pretty easy.
  58. @rbusarow val dataModule = module { } Modules Modules are

    created using the keyword `module`. There is no hierarchy to modules. Can’t be nested. Let’s add our first dependency. We need to create a Repository, right?
  59. @rbusarow val dataModule = module { factory { } }

    Modules We start by adding the `factory` keyword. Factory means that Koin will be creating a new instance each time it’s asked for a Repository.
  60. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules Then we define how to create the Repository inside the curly braces. This is a DSL sometimes DSL’s can look like magic but this is just a lambda We can put whatever we want in there as long as it returns what we need…
  61. @rbusarow val dataModule = module { factory { Timber.v("Hello everyone!")

    UserRepository().also { Timber.v("Goodbye!") } } } Modules Though obviously I wouldn’t recommend this.
  62. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules So now we’ve…. Defined our first dependency…? These things we’re putting in the module have a couple different names.
  63. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules BeanDefinition Component The actual class created is called a BeanDefinition In the documentation and his talks, Arnaud often calls it a Component I prefer Component, but it’s important to note that it’s nothing like a Dagger Component.
  64. @rbusarow Connecting the Graph Now that we have our first

    Component defined, we need to let Koin know about it…
  65. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } @rbusarow Connecting the Graph When I defined our Koin instance, I passed in this object. I called it a list of modules.
  66. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } @rbusarow Connecting the Graph
  67. @rbusarow val koinDemoModules: List<Module> get() = emptyListOf() val dataModule =

    module { factory { UserRepository() } } Connecting the Graph It makes sense to define these as a list somewhere, just for the sake of re-use and testing. - especially testing, but we’ll talk about that later. So all we have to do to add the Repository to the graph…
  68. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule) val dataModule =

    module { factory { UserRepository() } } Connecting the Graph … is to add the dataModule to the list. But that Repository has dependencies. They’re currently hard-coded in the class. Let’s take a look again…
  69. @rbusarow class UserRepository { internal val database: Database = Database.init(APP_CONTEXT)

    internal val okHttpClient: OkHttpClient = OkHttpClient() internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Connecting the Graph Now we can clean this up considerably…
  70. @rbusarow class UserRepository( private val service: UserService, private val dao:

    UserDao ) { … } Connecting the Graph We can just inject the two real dependencies. Note that we can also make them private, since in our testing we’ll already have a reference to them.
  71. @rbusarow val dataModule = module { factory { UserRepository() }

    } Connecting the Graph Now we’ve lost our no-arg constructor for Repository. It takes two parameters.
  72. @rbusarow val dataModule = module { factory { Repository(service =

    get(), dao = get()) } } Modules We can access those parameters from the graph by using `get()` get() is used to eagerly fetch instances of a dependency. It’s an inline reified function, so it knows the KClass being requested, which is how it resolves the dependency. But this won’t work because we don’t actually have those in the graph yet.
  73. @rbusarow val dataModule = module { factory<UserService> { TODO() }

    factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } Connecting the Graph Now we have them in the graph, but we need to add their own dependencies. Let’s fill out the Service first.
  74. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph Note that the Retrofit factory is able to do configurations directly within the component. Also note that the components are able to reference each other by supplying type parameters.
  75. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  76. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph Here we’ve added everything for the Dao, so the dependency graph is again complete. Two things to note We’re using androidApplication() to provide the application context We’re not actually creating a Dao. We’re just doing a lookup from the Database.
  77. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  78. @rbusarow Connecting the Graph val dataModule = module { factory

    { OkHttpClient() } factory { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } } This module is now a little difficult to read. Even though we don’t need them, I like to specify type parameters for legibility.
  79. @rbusarow Modules val dataModule = module { factory<OkHttpClient> { OkHttpClient()

    } factory<Retrofit> { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory<UserRepository> { UserRepository(service = get(), dao = get()) } } Now the types are all nicely aligned.
  80. val dataModule = module { factory<OkHttpClient> { OkHttpClient() } factory<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory<UserRepository> { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph Next, we have to look at this module’s efficiency. Factory creates a new instance each time it’s invoked. We don’t really want that for any of these, except the Dao. The Dao isn’t actually being created. It’s just being accessed from the database. We don’t need to retain its instance anywhere else. There’s another keyword we can use to create singleton instances…
  81. val dataModule = module { single<OkHttpClient> { OkHttpClient() } single<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } single<UserService> { get<Retrofit>().create<UserService>() } single<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } single<UserRepository> { UserRepository(service = get(), dao = get()) } } @rbusarow Modules All we have to do is change factory to single, and now the states will persist. Prior to Koin 2.0, this was called “bean”. Now “bean” is aliased to “single”. that’s kind of keyword #6 but not really.
  82. @rbusarow Accessing the Graph So the graph is able to

    access itself. What about OUTSIDE
  83. @rbusarow class UserScreen { val repository: UserRepository = TODO() }

    Accessing the Graph Let’s say you have a screen. It isn’t part of a class heirarchy. For some reason, it wants a Repository. How does it get it?
  84. @rbusarow class UserScreen { val repository = GlobalContext.get().koin.get<UserRepository>() } Accessing

    the Graph Since we have a global Koin instance, we can access that. But this is pretty gross. Fortunately there’s a shortcut…
  85. @rbusarow class UserScreen : KoinComponent { val repository = get<UserRepository>()

    } Accessing the Graph just tag any class with KoinComponent. convenience marker interface has extension functions which delegate the “get” to the global context. But it gets better.
  86. @rbusarow class UserScreen : Activity() { val repository = get<UserRepository>()

    } Accessing the Graph In Android, there are also extension functions for the ComponentCallbacks interface. Fragments, Activities, and Services get the same functionality as KoinComponent.
  87. @rbusarow class UserScreen : Activity() { val repository = get<UserRepository>()

    } class UserViewModel( private val repository: UserRepository ) : ViewModel() Accessing the Graph But the Screen shouldn’t be dealing with Repositories. That’s the ViewModel’s job. So let’s create the ViewModel, then add it to our graph…
  88. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule) val viewModelModule =

    module { viewModel<UserViewModel> { UserViewModel(repository = get()) } } Accessing the Graph Viewmodels in Android get a new keyword - “viewModel”. This component is the similar a singleton, except that it puts the ViewModel in a different scope which is all its own.
  89. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val viewModelModule

    = module { viewModel<UserViewModel> { UserViewModel(repository = get()) } } Accessing the Graph And we can’t forget to add it to our list of modules.
  90. @rbusarow class UserScreen : Activity() { val viewModel = get<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph Now the Screen can access a ViewModel as well. Except that we need the ViewModel to be tied to the Activity’s Lifecycle And we can’t access it immediately after init. We can use the viewModel keyword for that as well…
  91. @rbusarow class UserScreen : Activity() { val viewModel by viewModel<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph This viewModel delegate is a lazy delegate. Koin will now observe the lifecycle of the Activity, and destroy the ViewModel when the Activity is destroyed.
  92. @rbusarow class UserScreen : Activity() { val presenter by inject<UserPresenter>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph For everything which isn’t a viewModel, we have another keyword. “inject” returns a lazy property. We can use this in Android components, KoinComponents, or by directly accessing a Koin instance.
  93. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, coroutineScopeModule) val coroutineScopeModule

    = module { factory<CoroutineScope> { CoroutineScope(Dispatchers.Main + SupervisorJob()) } factory<CoroutineScope> { CoroutineScope(Dispatchers.IO + SupervisorJob()) } } Qualifiers What if we wanted to provide some coroutineScopes to our graph?
  94. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, coroutineScopeModule) val coroutineScopeModule

    = module { factory<CoroutineScope> { CoroutineScope(Dispatchers.Main + SupervisorJob()) } factory<CoroutineScope> { CoroutineScope(Dispatchers.IO + SupervisorJob()) } } Qualifiers They’re of the same type, so we’ll get an exception for duplicate bindings
 
 We need to provide some way for Koin to differentiate them and our requests
  95. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, coroutineScopeModule) val coroutineScopeModule

    = module { factory<CoroutineScope>(named("MainScope")) { CoroutineScope(Dispatchers.Main + SupervisorJob()) } factory<CoroutineScope>(named("IOScope")) { CoroutineScope(Dispatchers.IO + SupervisorJob()) } } Qualifiers They’re of the same type, so we’ll get an exception for duplicate bindings
 
 We need to provide some way for Koin to differentiate them and our requests
  96. @rbusarow class UserScreen : Activity() { val mainScope = get<CoroutineScope>(named("MainScope"))

    val viewModel by viewModel<UserViewModel>() } Qualifiers When we want to access these qualified properties, we pass the “named” in to the parameters of the “get”, “inject” or “viewModel” functions.
  97. @rbusarow Parameters What about constructors which require runtime conditions, like

    an ID? These have always been awkward for Dagger. How does Koin handle them?
  98. @rbusarow class UserScreen : Activity() { val repository by inject<UserViewModel>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserViewModel) : ViewModel() Parameters I said our ViewModel takes a user Id.
  99. @rbusarow class UserScreen : Activity() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() Parameters Now we have to add it to the module definition
  100. @rbusarow class UserScreen : Activity() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() val viewModelModule = module { viewModel<UserViewModel> { (userId: String) -> ViewModel(userId = userId, repository = get()) } } Parameters Remember that I said a component just takes a lambda. You can include parameters in that lambda, then pass them to the dependency.
  101. @rbusarow class UserScreen : Activity() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel> { parametersOf(“50 Cent”) } } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() val viewModelModule = module { viewModel<UserViewModel> { (userId: String) -> UserViewModel(userId = userId, repository = get()) } } Parameters Now, to pass them from the Screen, we use “parametersOf” in a lambda. And that’s it. This is significantly cleaner than a Dagger implementation.
  102. @rbusarow Scope Now let’s talk about Scope. We already have

    been. We’ve been discussing the extremes - factories and singletons. Sometimes we want to have a scope which is in between. Objects in a scope persist until the scope is closed.
  103. @rbusarow Scope In order to create a scoped dependency, we

    use a Scope builder. The scope builder takes a name, which is an identifier for that type of scope. Then inside it, we declare components which are tied to that scope. And we add the new module to our graph.
  104. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val scopedModule

    = module { scoped<Preferences> { Preferences() } } Scope In order to create a scoped dependency, we use a scoped keyword. This is on the same level as factory, single, viewmodel, etc.
  105. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val scopedModule

    = module { scope(scopeName = named("login")) { scoped<Preferences> { Preferences() } } } Scope The scoped keyword goes inside a ScopeSet builder, which takes a qualifier for the type of scope. Then inside it, we declare components which are tied to that scope.
  106. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = named("login")) { scoped<Preferences> { Preferences() } scoped<Diary> { Diary() } } } Scope As many components as we want.
  107. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = named("login")) { scoped<Preferences> { Preferences() } scoped<Diary> { Diary() } } } Scope And of course we add the new module to our graph.
  108. @rbusarow class Anywhere : KoinComponent { val loginScope = createScope(

    id = named("login"), qualifier = named("50 cent") ) val preferences by loginScope.inject<Preferences>() } Scope We can create that scope anywhere, and use it to access any dependencies from inside the scope. One caveat is that when the user logs out, we need to explicitly close the scope. And of course, for Android we get something special:
  109. @rbusarow class Screen : Activity() { val presenter by currentScope.inject<UserPresenter>()

    val viewModel by viewModel<ViewModel>() } val scopedModule = module { scope(named<Screen>()) { scoped<UserPresenter> { UserPresenter() } } scope(scopeName = named("login")) { … } } Parameters We have an extension function to create a scope from any lifecycle owner. Its name will be derived from the type of the class. So we can create a Scope using the type of the class, add its dependencies and then in the screen, we can just use “currentScope”!
  110. @rbusarow Dynamic Features If you’re using dynamic feature modules, this

    is somewhat tricky. Applications don’t have any knowledge of the feature module. So how do you load those modules to your graph?
  111. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature Dynamic Features
  112. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature class SomeFeatureActivity : Activity() { init { initModules() } } Dynamic Features
  113. @rbusarow internal class UserRepositoryTest { val service = mockk<UserService>() val

    dao = mockk<UserDao>() lateinit var repo: UserRepository @BeforeEach fun beforeEach() { repo = UserRepository(service, dao) } } Testing With most dependency injection, with tests, you have to create your dependencies from scratch.
  114. @rbusarow internal class UserRepositoryTest : KoinTest { val service =

    mockk<UserService>() val dao = mockk<UserDao>() lateinit var repo: UserRepository @BeforeEach fun beforeEach() { repo = UserRepository(service, dao) } } Testing With Koin, we can add the KoinTest interface. This is similar to the KoinComponent It’s a marker which gives us access to the whole graph.
  115. @rbusarow internal class UserRepositoryTest : KoinTest { … @BeforeEach fun

    beforeEach() { startKoin { modules(koinDemoModules) } } @AfterEach fun afterEach() { getKoin().close() } } Testing Now in the setup function, we call startKoin and add whatever modules we’d like Then in tearDown, we close the instance
  116. @rbusarow internal class UserRepositoryTest : KoinTest { lateinit var repo:

    UserRepository @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) } repo = get<UserRepository>() } @AfterEach fun afterEach() { getKoin().close() } } Testing Then, we can use Koin to get the dependency automatically. But wait - there’s more…
  117. @rbusarow internal class UserRepositoryTest : KoinTest { lateinit var repo:

    UserRepository val mocks = module(override = true) { single<UserService> { service } single<UserDao> { dao } } @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) } repo = get<UserRepository>() } @AfterEach fun afterEach() { getKoin().close() } } Testing We can override modules in Koin. Anything redefined in an overridden module will be available in the graph. The rest of the graph will behave normally.
  118. @rbusarow internal class UserRepositoryTest : KoinTest { lateinit var repo:

    UserRepository val mocks = module(override = true) { single<UserService> { service } single<UserDao> { dao } } @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) modules(mocks) } repo = get<UserRepository>() } … } Testing After defining your overrides, you need to add the module to your graph.
  119. @rbusarow internal class ModuleTest : KoinTest { @BeforeEach fun beforeEach()

    { startKoin { modules(koinDemoModules) } } @AfterEach fun afterEach() { getKoin().close() } @Test fun `check modules`() { getKoin().checkModules() } } Testing “checkModules” does exactly that. It goes through the entire graph, as defined in all modules.
  120. @rbusarow val dataModule = module { single<OkHttpClient> { OkHttpClient() }

    single<Retrofit> { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } single<UserService> { get<Retrofit>().create<UserService>() } single<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } single<UserRepository> { UserRepository(service = get(), dao = get()) } } Testing Every dependency required by a component in a module must resolve. This respects qualifiers and scopes.
  121. @rbusarow class Screen : Activity() { val diary by loginScope.inject<Diary>()

    val viewModel by viewModel<ViewModel>() } Testing Any access outside of the modules is not checked. Calling inject/get/viewmodel in a class is a leap of faith.
  122. @rbusarow class Screen : Activity() { val diary by loginScope.inject<Diary>()

    val viewModel by viewModel<ViewModel>() } Testing In this case, Diary would crash because it isn’t provided anywhere in the graph.
  123. @rbusarow class Screen : Activity() { val diary by loginScope.inject<Diary>()

    val viewModel by viewModel<ViewModel>() } Testing CRASH!!
  124. @rbusarow class Screen : Activity() { @Inject lateinit var diary:

    Diary val viewModel by viewModel<ViewModel>() } Testing Note that this also happens with Dagger.
  125. @rbusarow class Screen : Activity() { @Inject lateinit var diary:

    Diary val viewModel by viewModel<ViewModel>() } Testing CRASH!!
  126. @rbusarow Simple The DSL is small Any questions can be

    answered by clicking through the code The code isn’t generated There’s no magic Excellent documentation
  127. @rbusarow Safe CheckModules does check the graph Private immutable vals

    in classes where we can’t use the constructor
  128. @rbusarow Extensible Nothing is generated at compile time Easy to

    create your own version of just about anything
  129. @rbusarow Further Reading http://bit.ly/322XZr5 http://bit.ly/2L2IfO4 Koin Android documentation: Koin Core

    documentation: Koin Github: http://bit.ly/2ZkTk2d Slides: http://bit.ly/2okYM8C