Share
Embed
Dependency Injection with KODEIN
Presentation about Dependency Injection fundamentals and how to achieve DI in Kotlin-powered applications using the KODEIN framework.
Presentation given at
- Kotlin Meetup SP #02 (January/2018)
Ubiratan Soares PRO
January 11, 2018
Transcript
50 slides
-
Dependency Injection is all about a separation of concerns between
some object creation and usage -
class Controller(private val source: DataSource) { fun doSomeWork() { val
service = Service(ds) val formatter = OutputFormatter() render(formatter.format(service.getResults())) } } -
class Controller(private val source: DataSource) { fun doSomeWork() { val
service = Service(ds) val formatter = OutputFormatter() render(formatter.format(service.getResults())) } } -
class Controller( private val service: Service, private val formatter: OutputFormatter)
{ fun doSomeWork() { val results = service.getResults() render(formatter.format(results)) } } class Service(private val source: DataSource) { } class DataSource( private val logger: Logger, private val tracker: ErrorTracker, // other dependencies ) class OutputFormatter { } -
class Controller( private val service: Service, private val formatter: OutputFormatter)
{ fun doSomeWork() { val results = service.getResults() render(formatter.format(results)) } } class Service(private val source: DataSource) { } class DataSource( private val logger: Logger, private val tracker: ErrorTracker, // other dependencies ) class OutputFormatter { } -
Enforcing DI means that any class with an available public
constructor gets all its collaborators at instantiation time -
Data Source Controller Service External RestAPI Error Tracker Logger Data
Parser Cache Colaborates with -
Dependencies Chain Controller A Dependencies Chain Controller B Dependencies Chain
Controller N Application … -
• Manual code setup of all objects creation and DAG
relationships • Extra code needed for scoped- based dependencies • Reflection-free • No DI errors at runtime • Boilterplate • Semi automated code setup, driven by JSR330 annotations standard • Reflection-based (Guice, etc) or reflection-free (Dagger2) • Reflection-based solutions may crash at runtime due to dependency resolution errors • Less code than static factories Static Factories JSR330 -
Dependency Injection and Dependency Inversion are two distinct concepts, but
related in practice -
The Dependency Inversion Principle (from SOLID) "High level policies should
not depend directly on low-level details; instead, low-level details should depend on high level policies. To accomplish this, runtime dependencies can be decoupled from source code dependencies via a shared polymorphic abstraction" -
interface Logger { fun log(severity: Level, toLog : String) }
class SL4JLogger : Logger { override fun log(severity: Level, toLog: String) { // Print to console } } class LogEntriesLogger : Logger { override fun log(severity: Level, toLog: String) { // Send to external logging service } } -
OVERVIEW • 100% Kotlin dependency injection framework • Heavily based
on Kotlin features : type system and lazy properties • Reflection-based • Mirrors some API style from Guice, but has its own style as well • Supports Kotlin-JVM, Android, JS and Kotlin-native -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... The inferred LazyKodein subtype represents the object graph -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... Instances will be created in a lazy way -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... Defines a binding, eg, a way to provide an object instance -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... Defines the lifecycle time - or scope - of this object -
object Injector { val graph = Kodein.lazy { bind<Gson>() with
singleton { Gson() } bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider { RestCaller(httpClient = instance()) } // More bindings ... Pulls a dependency transitively from the graph, with a proper scope -
bind<APIGateway>() with provider { APIGateway( homeService = instance(), brokerService =
instance(), walletService = instance() ) } You should use named parameters to clarify the dependencies that are being pulled transitively -
bind<Scheduler>(UITHREAD) with singleton { AndroidSchedulers.mainThread() } bind<Scheduler>(WORKER) with singleton {
Schedulers.io() } bind<SomeBussinessRule>() with provider { SomeInfrastructure( storage = instance(), webService = instance(), worker = instance(WORKER) ) } companion object { val WORKER = "worker" val UITHREAD = "main" } -
You can bind multiple instances of same type using tags
bind<Scheduler>(UITHREAD) with singleton { AndroidSchedulers.mainThread() } bind<Scheduler>(WORKER) with singleton { Schedulers.io() } bind<SomeBussinessRule>() with provider { SomeInfrastructure( storage = instance(), webService = instance(), worker = instance(WORKER) ) } companion object { val WORKER = "worker" val UITHREAD = "main" } -
bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider {
RestCaller(httpClient = instance()) } -
bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider {
RestCaller(httpClient = instance()) } This dependency will live as long the Kodein instance lives -
bind<OkHttpClient>() with singleton { OkHttpClient.Builder().build() } bind<RestCaller>() with provider {
RestCaller(httpClient = instance()) } Each time this dependency will be required, a new instance will be created -
You may have a dependency bounded with a custom scope,
eg, this dependency will live as long the provided object lives bind<Presenter>() with scopedSingleton(androidActivityScope) { Presenter( scheduler = instance(UITHREAD), view = it ) } The object itself is accessible at binding time -
fun main(args: Array<String>) { val gateway: APIGateway by Injector.graph.instance() gateway.start()
} Our class holds the LazyKodein property No class reference needed to retrieve an instance from the graph -
class App : Application(), KodeinAware { override val kodein by
Injection(context = this).graph } class SomeActivity : AppCompatActivity(), SomeView { private val kodein by lazy { LazyKodein(appKodein) } private val presenter by kodein.with(this).instance<Presenter>() // Do your work … } -
class App : Application(), KodeinAware { override val kodein by
Injection(context = this).graph } class SomeActivity : AppCompatActivity(), SomeView { private val kodein by lazy { LazyKodein(appKodein) } private val presenter by kodein.with(this).instance<Presenter>() // Do your work … } This interface defines an instance that can hold a dependency graph -
class App : Application(), KodeinAware { override val kodein by
Injection(context = this).graph } class SomeActivity : AppCompatActivity(), SomeView { private val kodein by lazy { LazyKodein(appKodein) } private val presenter by kodein.with(this).instance<Presenter>() // Do your work … } Provided by the Android support -
class App : Application(), KodeinAware { override val kodein by
Injection(context = this).graph } class SomeActivity : AppCompatActivity(), SomeView { private val kodein by lazy { LazyKodein(appKodein) } private val presenter by kodein.with(this).instance<Presenter>() // Do your work … } Provides the custom dependency scope -
MUCH MORE • More ways to provide and retrieve dependencies
• More ways to define dependencies scopes • Modules support, including dependency overriding • Multibindings support • JSR330 inter-operability for injections • Great documentation -
CONCLUSIONS • Kodein is a great Kotlin first-class DI framework
• You dont have to follow any JSR330 conventions to define the object graph • Kodein has same pain-points that reflection-based DI framework have : not so fast when compared with reflection-free solutions, errors may occur at runtime due to dependencies resolution failures • Great alternative for DI on Android applications -
UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy
Google Developer Expert for Android Teacher, speaker, etc, etc