Getting Big Payouts with Koin -- droidcon Lisbon

0c062ecd54ead5cce8b2b79acbe557d8?s=47 Rick Busarow
September 09, 2019

Getting Big Payouts with Koin -- droidcon Lisbon

Thanks to its clean design, Koin is quickly gaining popularity as a service locator framework. Highlights include its excellent documentation, accessible api, and the pure Kotlin source code.

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. Then, and most importantly, we’ll see how we can use Koin to quickly and easily generate mocked dependencies for clean and hermetic tests.

0c062ecd54ead5cce8b2b79acbe557d8?s=128

Rick Busarow

September 09, 2019
Tweet

Transcript

  1. @rbusarow GETTING BIG PAYOUTS WITH KOIN Rick Busarow rickbusarow@gmail.com

  2. @rbusarow What’s Koin?

  3. What’s Koin? @rbusarow

  4. What’s Koin? @rbusarow

  5. What’s Koin? @rbusarow

  6. @rbusarow Screen ViewModel UserId Repository Dao Room Database App Context

    Service Retrofit OkHttpClient Base Url
  7. @rbusarow Normal Control Flow

  8. @rbusarow class UserRepository { internal val service: UserService = retrofit.create<UserService>()

    internal val dao: UserDao = database.userDao } Normal Control Flow
  9. @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
  10. @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
  11. @rbusarow Inversion of Control

  12. @rbusarow Service Location Dependency Injection Inversion of Control

  13. @rbusarow Service Location Dependency Injection class UserViewModel { private val

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

    repository = Locator.createRepository() } class UserViewModel(val repository: UserRepository) { lateinit var otherDependency : OtherDependency } Inversion of Control
  15. @rbusarow Field Injection

  16. @rbusarow class UserScreen : Fragment() { lateinit var viewModel: UserViewModel

    } Field Injection
  17. class UserScreen : Fragment() { lateinit var viewModel: UserViewModel override

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

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

    override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } Field Injection public mutable late init
  20. @rbusarow Constructor Injection

  21. @rbusarow class UserRepository( private val service: UserService, private val dao:

    UserDao ) { … } Constructor Injection
  22. @rbusarow class UserViewModel { val repository = UserRepository(service = TODO(),

    dao = TODO()) } Constructor Injection
  23. @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) } Constructor Injection
  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) } Constructor Injection
  25. @rbusarow class UserViewModel(service: UserService, dao: UserDao) { val repository =

    UserRepository(service = service, dao = dao) } Constructor Injection
  26. @rbusarow class UserViewModel( private val repository: UserRepository ) : ViewModel()

    Constructor Injection
  27. @rbusarow class UserScreen : Fragment() { val viewModel = UserViewModel(repository

    = TODO()) } Constructor Injection
  28. @rbusarow class UserScreen : Fragment() { lateinit var viewModel :

    UserViewModel override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Field Injection…
  29. @rbusarow class UserScreen : Fragment() { lateinit var viewModel :

    UserViewModel override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Field Injection…
  30. @rbusarow Service Location

  31. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { ViewModelProviders .of(this) .get(UserViewModel::class.java) } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location
  32. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location
  33. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location object Locator { inline fun <reified T : ViewModel> get(): T = … }
  34. @rbusarow class UserScreen : Fragment() { private val presenter by

    lazy { Locator.get<UserPresenter>() } private val viewModel by lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location object Locator { inline fun <reified T : ViewModel> get(): T = … }
  35. @rbusarow class UserScreen : Fragment() { private val presenter by

    lazy { Locator.instance.get<UserPresenter>() } private val viewModel by lazy { Locator.instance.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } interface Locator { fun <T : ViewModel> get(): T = … companion object { lateinit var instance: Locator } } Service Location
  36. @rbusarow Koin KoinApplication

  37. 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
  38. 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
  39. 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
  40. @rbusarow GlobalContext

  41. @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
  42. @rbusarow startKoin( ) koinApplication( )

  43. @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
  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
  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
  46. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() } } MyApplication.kt
  47. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() startKoin { // route all Koin logs through android.util.Log androidLogger() } } } MyApplication.kt
  48. @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
  49. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() } } } MyApplication.kt
  50. @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
  51. @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
  52. @rbusarow Modules

  53. @rbusarow val dataModule = module { } Modules

  54. @rbusarow val dataModule = module { factory { } }

    Modules
  55. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules
  56. @rbusarow val dataModule = module { factory { Timber.v("Hello everyone!")

    UserRepository().also { Timber.v("Goodbye!") } } } Modules
  57. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules
  58. @rbusarow val dataModule = module { factory { UserRepository() }

    } Modules BeanDefinition Component
  59. @rbusarow Connecting the Graph

  60. 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
  61. 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
  62. @rbusarow val koinDemoModules: List<Module> get() = emptyListOf() val dataModule =

    module { factory { UserRepository() } } Connecting the Graph
  63. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule) val dataModule =

    module { factory { UserRepository() } } Connecting the Graph
  64. @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
  65. @rbusarow class UserRepository( private val service: UserService, private val dao:

    UserDao ) { … } Connecting the Graph
  66. @rbusarow val dataModule = module { factory { UserRepository() }

    } Connecting the Graph
  67. @rbusarow val dataModule = module { factory { Repository(service =

    get(), dao = get()) } } Modules
  68. @rbusarow val dataModule = module { factory<UserService> { TODO() }

    factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } Connecting the Graph
  69. 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
  70. 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
  71. 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
  72. 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
  73. @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()) } }
  74. @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()) } }
  75. 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
  76. 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
  77. @rbusarow Accessing the Graph

  78. @rbusarow class UserScreen { val repository: UserRepository = TODO() }

    Accessing the Graph
  79. @rbusarow class UserScreen { val repository = GlobalContext.get().koin.get<UserRepository>() } Accessing

    the Graph
  80. @rbusarow class UserScreen : KoinComponent { val repository = get<UserRepository>()

    } Accessing the Graph
  81. @rbusarow class UserScreen : Fragment() { val repository = get<UserRepository>()

    } Accessing the Graph
  82. @rbusarow class UserScreen : Fragment() { val repository = get<UserRepository>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  83. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val viewModelModule

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

    = module { viewModel<ViewModel> { ViewModel(repository = get()) } } Accessing the Graph
  85. @rbusarow class UserScreen : Fragment() { val viewModel = get<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  86. @rbusarow class UserScreen : Fragment() { val viewModel by viewModel<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  87. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  88. @rbusarow Parameters

  89. @rbusarow class UserScreen : Fragment() { val repository by inject<UserViewModel>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserViewModel) : ViewModel() Parameters
  90. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() Parameters
  91. @rbusarow class UserScreen : Fragment() { 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
  92. @rbusarow class UserScreen : Fragment() { 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
  93. @rbusarow Scope

  94. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scoped<Preferences> { Preferences() } } Scope
  95. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

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

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

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

    val viewModel by viewModel<ViewModel>() } val scopedModule = module { scope(named<Screen>()) { scoped<Presenter> { Presenter() } } scope(scopeName = Scopes.login) { scoped<Preferences> { Preferences() } } } Parameters
  99. @rbusarow Dynamic Features

  100. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) Dynamic Features

  101. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

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

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

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature class SomeFeatureActivity : Activity() { init { initModules() } } Dynamic Features
  104. @rbusarow Testing

  105. @rbusarow Overriding Modules

  106. @rbusarow internal class UserRepositoryTest { val service = mockk<UserService>() val

    dao = mockk<UserDao>() lateinit var repo: UserRepository @BeforeEach fun beforeEach() { repo = UserRepository(service, dao) } } Testing
  107. @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
  108. @rbusarow internal class UserRepositoryTest : KoinTest { … @BeforeEach fun

    beforeEach() { startKoin { modules(koinDemoModules) } repo = get<UserRepository>() } @AfterEach fun afterEach() { getKoin().close() } } Testing
  109. @rbusarow internal class UserRepositoryTest : KoinTest { … 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
  110. @rbusarow internal class UserRepositoryTest : KoinTest { … val mocks

    = module(override = true) { single<UserService> { service } single<UserDao> { dao } } @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) modules(mocks) } repo = get<UserRepository>() } … } Testing
  111. @rbusarow Checking the Graph

  112. @rbusarow internal class ModuleTest : KoinTest { @BeforeEach fun beforeEach()

    { startKoin { modules(koinDemoModules) } } @AfterEach fun afterEach() { getKoin().close() } @Test fun `check modules`() { getKoin().checkModules() } } Testing
  113. @rbusarow Try it yourself!

  114. @rbusarow Further Reading http://bit.ly/322XZr5 http://bit.ly/2L2IfO4 Koin Android documentation: Koin Core

    documentation: Koin Github: http://bit.ly/2ZkTk2d
  115. rbusarow rbusarow rbusarow THANKS! Rick Busarow rickbusarow@gmail.com