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

Investing in Koin

Investing in Koin

Thanks to its clean design, Koin is quickly gaining popularity as an IoC 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

August 23, 2019
Tweet

More Decks by Rick Busarow

Other Decks in Programming

Transcript

  1. @rbusarow class MyRepository { internal val database: MyDatabase = MyDatabase.init(APP_CONTEXT)

    internal val okHttpClient: OkHttpClient = OkHttpClient() internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val service: MyService = retrofit.create<MyService>() internal val dao: MyDao = TODO() } Normal Control Flow
  2. @rbusarow Service Location Dependency Injection class ViewModel { val repository

    = Locator.createRepository() } Inversion of Control
  3. @rbusarow Service Location Dependency Injection class ViewModel { val repository

    = Locator.createRepository() } class ViewModel(val repository: Repository) { lateinit var otherDependency : OtherDependency } Inversion of Control
  4. Inversion of Control @rbusarow class ViewModel(locator: Locator) { val repository

    = locator.createRepository() } class ViewModel(val repository: Repository) { lateinit var otherDependency : OtherDependency } Service Location Dependency Injection
  5. class Screen : Fragment() { lateinit var viewModel: ViewModel override

    fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: Screen) { screen.viewModel = TODO() } } @rbusarow Field Injection
  6. class Screen : Fragment() { lateinit var viewModel: ViewModel override

    fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: Screen) { screen.viewModel = TODO() } } @rbusarow Field Injection
  7. @rbusarow class Screen : Fragment() { lateinit var viewModel: ViewModel

    override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: Screen) { screen.viewModel = TODO() } } Field Injection
  8. @rbusarow class Screen : Fragment() { lateinit var viewModel: ViewModel

    override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: Screen) { screen.viewModel = TODO() } } Field Injection public mutable late init
  9. class Screen { val repository: Repository = TODO() val viewModel

    = ViewModel(repository) fun onCreate() { viewModel.observe { ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  10. class Screen(repository: Repository) { val viewModel = ViewModel(repository) fun onCreate()

    { viewModel.observe { ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  11. class Screen(val viewModel: ViewModel) { fun onCreate() { viewModel.observe {

    ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  12. class Screen(val viewModel: ViewModel) { fun onCreate() { viewModel.observe {

    ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  13. class Screen(val viewModel: ViewModel) { fun onCreate() { viewModel.observe {

    ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  14. class Screen { // lazy init val viewModel by ViewModelProvider.get<ViewModel>(this)

    fun onCreate() { viewModel.observe { ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  15. class Screen { // lazy init val viewModel by Locator.get<ViewModel>(this)

    fun onCreate() { viewModel.observe { ... } } } class ViewModel(val repository: Repository) @rbusarow Dependency Injection
  16. 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 } 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
  17. 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 } 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
  18. 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 } 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
  19. @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
  20. @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
  21. @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
  22. @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
  23. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) } } MyApplication.kt
  24. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through android.util.Log androidLogger() } } } MyApplication.kt
  25. @rbusarow private 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
  26. @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
  27. @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
  28. @rbusarow val dataModule = module { factory { Timber.v("Hello everyone!")

    MyRepository().also { Timber.v("Goodbye!") } } } Modules
  29. 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
  30. 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
  31. @rbusarow class MyRepository { internal val database: MyDatabase = MyDatabase.init(APP_CONTEXT)

    internal val okHttpClient: OkHttpClient = OkHttpClient() internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val service: MyService = retrofit.create<MyService>() internal val dao: MyDao = TODO() } Modules
  32. @rbusarow val dataModule = module { factory<MyService> { TODO() }

    factory<MyDao> { TODO() } factory { MyRepository(service = get(), dao = get()) } } Modules
  33. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory<MyDao> { TODO() } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  34. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory<MyDao> { TODO() } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  35. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  36. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  37. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  38. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  39. val dataModule = module { factory<OkHttpClient> { OkHttpClient() } factory<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory<MyDatabase> { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory<MyRepository> { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  40. val dataModule = module { factory<OkHttpClient> { OkHttpClient() } factory<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<MyService> { get<Retrofit>().create<MyService>() } factory<MyDatabase> { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } factory<MyRepository> { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  41. val dataModule = module { single<OkHttpClient> { OkHttpClient() } single<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } single<MyService> { get<Retrofit>().create<MyService>() } single<MyDatabase> { MyDatabase.init(androidApplication()) } factory<MyDao> { get<MyDatabase>().myDao } single<MyRepository> { MyRepository(service = get(), dao = get()) } } @rbusarow Modules
  42. @rbusarow class MyScreen : Fragment() { val repository = get<MyRepository>()

    } class MyViewModel(private val repository: MyRepository) : ViewModel() { } Accessing the Graph
  43. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule) val viewModelModule =

    module { viewModel<MyViewModel> { MyViewModel(repository = get()) } } Accessing the Graph
  44. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val viewModelModule

    = module { viewModel<MyViewModel> { MyViewModel(repository = get()) } } Accessing the Graph
  45. @rbusarow class MyScreen : Fragment() { val viewModel = get<MyViewModel>()

    } class MyViewModel(private val repository: MyRepository) : ViewModel() { } Accessing the Graph
  46. @rbusarow class MyScreen : Fragment() { val viewModel by viewModel<MyViewModel>()

    } class MyViewModel(private val repository: MyRepository) : ViewModel() { } Accessing the Graph
  47. @rbusarow class MyScreen : Fragment() { val repository by inject<MyRepository>()

    val viewModel by viewModel<MyViewModel>() } class MyViewModel(private val repository: MyRepository) : ViewModel() { } Accessing the Graph
  48. @rbusarow class MyScreen : Fragment() { val repository by inject<MyRepository>()

    val viewModel by viewModel<MyViewModel>() } class MyViewModel(private val repository: MyRepository) : ViewModel() { } Parameters
  49. @rbusarow class MyScreen : Fragment() { val repository by inject<MyRepository>()

    val viewModel by viewModel<MyViewModel>() } class MyViewModel( private val userId: Int, private val repository: MyRepository ) : ViewModel() Parameters
  50. @rbusarow class MyScreen : Fragment() { val repository by inject<MyRepository>()

    val viewModel by viewModel<MyViewModel>() } class MyViewModel( private val userId: Int, private val repository: MyRepository ) : ViewModel() val viewModelModule = module { viewModel<MyViewModel> { (userId: Int) -> MyViewModel(userId = userId, repository = get()) } } Parameters
  51. @rbusarow class MyScreen : Fragment() { val repository by inject<MyRepository>()

    val viewModel by viewModel<MyViewModel> { parametersOf(123) } } class MyViewModel( private val userId: Int, private val repository: MyRepository ) : ViewModel() val viewModelModule = module { viewModel<MyViewModel> { (userId: Int) -> MyViewModel(userId = userId, repository = get()) } } Parameters
  52. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = named("login")) { scoped { Preferences() } } } Scope
  53. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = named("login")) { scoped { Preferences() } } } Scope
  54. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = named("login")) { scoped { Preferences() } } } class Anywhere : KoinComponent { val userID = 153 val loginScope = getKoin().createScope(userID.toString(), named("login")) val preferences by loginScope.inject<Preferences>() } Scope
  55. @rbusarow class MyScreen : Fragment() { val presenter by currentScope.inject<Presenter>()

    val viewModel by viewModel<MyViewModel>() } val scopedModule = module { scope(named<MyScreen>()) { scoped { Presenter() } } scope(scopeName = named("login")) { scoped { Preferences() } } } Parameters
  56. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

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

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature class SomeFeatureActivity : Activity() { init { initModules() } } Dynamic Features
  58. @rbusarow internal class ModuleTest : KoinTest { @BeforeEach fun beforeEach()

    { startKoin { modules(koinDemoModules) } } @AfterEach fun afterEach() { getKoin().close() } @Test fun `check modules`() { getKoin().checkModules() } } Testing
  59. @rbusarow internal class RepositoryTest : KoinTest { val mockModule =

    module { single<MyDao>(override = true) { MockDao() } } @Test fun `MyRepository should return Dao contents`() { loadKoinModules(mockModule) val repo = get<MyRepository>() // repo is now using mocked Dao } } Testing