Share
Embed
DIY Dependency Injection with Kotlin @ DevFest Florida 2019
Talk Description: Dependency Injection seems like this SUPER complex topic that only the most senior developers understand. At the end of the day, it’s just “inversion of control” where instances of objects/classes are provided to you, instead of created by you. Kotlin’s language features allow us to do this concisely, without a library, and in this talk I’ll show you how to DO IT YOURSELF. This talk will demystify dependency injection and help you understand the huge benefits it provides for testing, maintainability and modularity.
NOTE: There is a lot of context you may be missing if you don't watch the recording of the talk. It is not available yet though as of 01/23/2019.
Sam Edwards
January 19, 2019
Transcript
110 slides
-
@HandstandSam #DevFestFL Why am I talking about DIY Dependency Injection
with Kotlin? ! Share the story of how we implemented DIY -
@HandstandSam #DevFestFL Why am I talking about DIY Dependency Injection
with Kotlin? ! Share the story of how we implemented DIY ! Demystify the topic of Dependency Injection -
@HandstandSam #DevFestFL Why am I talking about DIY Dependency Injection
with Kotlin? ! Share the story of how we implemented DIY ! Demystify the topic of Dependency Injection ! Show how you can implement DIY, if you choose -
@HandstandSam #DevFestFL Our Story ! Our team previously built Capital
One Wallet ! We got tasked to build a reusable SDK for Authentication -
@HandstandSam #DevFestFL Our Story ! Our team previously built Capital
One Wallet ! We got tasked to build a reusable SDK for Authentication ! This was the first time we were building a complex SDK -
@HandstandSam #DevFestFL Product Requirements ! SDK will be used by
different apps, in different codebases -
@HandstandSam #DevFestFL Product Requirements ! SDK will be used by
different apps, in different codebases ! Should not assume clients will be using a 3rd party library -
@HandstandSam #DevFestFL Product Requirements ! SDK will be used by
different apps, in different codebases ! Should not assume clients will be using a 3rd party library ! Stay lean and lightweight -
@HandstandSam #DevFestFL Product Requirements ! SDK will be used by
different apps, in different codebases ! Should not assume clients will be using a 3rd party library ! Stay lean and lightweight ! Make the SDK configurable -
@HandstandSam #DevFestFL Technical Requirements ! Reactive Programming ! Immutable Data
! Modular Code ! Single Responsibility Code -
@HandstandSam #DevFestFL Technical Requirements ! Reactive Programming ! Immutable Data
! Modular Code ! Single Responsibility Code ! Well Tested Code -
@HandstandSam #DevFestFL Technical Requirements ! Reactive Programming ! Immutable Data
! Modular Code ! Single Responsibility Code ! Well Tested Code ! Avoid 3rd Party Libraries -
@HandstandSam #DevFestFL Technical Requirements ! Reactive Programming ! Immutable Data
! Modular Code ! Single Responsibility Code ! Well Tested Code ! Avoid 3rd Party Libraries ! 100% Kotlin -
@HandstandSam #DevFestFL Technical Requirements ! Reactive Programming ! Immutable Data
! Modular Code ! Single Responsibility Code ! Well Tested Code ! Avoid 3rd Party Libraries ! 100% Kotlin ! Dependency Injection -
@HandstandSam #DevFestFL Why Dependency Injection? ! Testing ! Mock Flavors
! Custom Debug Features -
@HandstandSam #DevFestFL Why Dependency Injection? ! Testing ! Mock Flavors
! Custom Debug Features ! Environment Switching (Test vs Production Servers) -
@HandstandSam #DevFestFL Why Dependency Injection? ! Testing ! Mock Flavors
! Custom Debug Features ! Environment Switching (Test vs Production Servers) ! Promotes building more reusable code -
@HandstandSam #DevFestFL Why Dependency Injection? ! Testing ! Mock Flavors
! Custom Debug Features ! Environment Switching (Test vs Production Servers) ! Promotes building more reusable code ! Discourages use of static singletons -
@HandstandSam #DevFestFL Why Dependency Injection? ! Testing ! Mock Flavors
! Custom Debug Features ! Environment Switching (Test vs Production Servers) ! Promotes building more reusable code ! Discourages use of static singletons ! It enables all the really cool things I like to talk about -
@HandstandSam #DevFestFL Dependency Injection is Passing in dependencies instead of
pre-defining them. Inversion of Control -
@HandstandSam #DevFestFL Without Dependency Injection class NetworkConfig( val fullUrl =
"http://localhost:8080/" val isLocalhostServer = true } -
@HandstandSam #DevFestFL Setter Injection class NetworkConfig( var fullUrl: String, var
isLocalhostServer: Boolean ) val config = NetworkConfig() config.fullUrl = “http://localhost:8080/" config.isLocalhostServer = true -
@HandstandSam #DevFestFL Constructor Injection class NetworkConfig( val baseUrl: String, val
port: Int ) { val fullUrl = "$baseUrl:$port/" val isLocalhostServer = baseUrl.contains("//localhost") } -
@HandstandSam #DevFestFL Commonalities: Inversion of Control ! Inversion of Control
is passing in dependencies instead of pre- defining them. -
@HandstandSam #DevFestFL Aren’t Their Libraries That Do DI for You?
! Yes, let’s take a look. -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO ❌ ❌ -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO Single Instance NO NO YES NO ❌ ❌ -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO Single Instance NO NO YES NO ❌ ❌ ❌ -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO Single Instance NO NO YES NO Available at the time YES YES YES NO ❌ ❌ ❌ -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO Single Instance NO NO YES NO Available at the time YES YES YES NO ❌ ❌ ❌ ❌ -
@HandstandSam #DevFestFL ! Dagger ! Toothpick ! Koin ! Kodein
Code Generation YES YES NO NO Single Instance NO NO YES NO Available at the time YES YES YES NO Adds dependency on another library YES YES YES YES ❌ ❌ ❌ ❌ -
@HandstandSam #DevFestFL So, How Much Work is DIY? ! Not
that much ! Slightly more verbose -
@HandstandSam #DevFestFL So, How Much Work is DIY? ! Not
that much ! Slightly more verbose ! Have to manually build advanced DI features -
@HandstandSam #DevFestFL So, How Much Work is DIY? ! Not
that much ! Slightly more verbose ! Have to manually build advanced DI features ! Great for simple use cases when you don’t already know a DI library really well -
@HandstandSam #DevFestFL Swapping Dependencies on a Graph ! Mock Server
vs. Testing Server vs. Live (Production Server) -
@HandstandSam #DevFestFL Swapping Dependencies on a Graph ! Mock Server
vs. Testing Server vs. Live (Production Server) ! Graph is where all your dependencies are configured. -
@HandstandSam #DevFestFL Swapping Dependencies on a Graph ! Mock Server
vs. Testing Server vs. Live (Production Server) ! Graph is where all your dependencies are configured. ! Just swap out pieces of your graph you need to change. -
@HandstandSam #DevFestFL Swapping Dependencies on a Graph ! Mock Server
vs. Testing Server vs. Live (Production Server) ! Graph is where all your dependencies are configured. ! Just swap out pieces of your graph you need to change. ! Or provide a brand new graph with the required fields. -
@HandstandSam #DevFestFL Network Graph interface NetworkGraph { val categoryRepo: CategoryRepo
val itemRepo: ItemRepo val userRepo: UserRepo } -
@HandstandSam #DevFestFL Network Graph interface NetworkGraph { val categoryRepo: CategoryRepo
val itemRepo: ItemRepo val userRepo: UserRepo } class NetworkGraphImpl( networkConfig: NetworkConfig ) : NetworkGraph { private val retrofit: Retrofit= Retrofit.Builder() .baseUrl(networkConfig.fullUrl) .addConverterFactory(moshiConverterFactory) .addCallAdapterFactory(rxJava2CallAdapterFactory) .client(okHttpClientBuilder.build()) .build() private val shoppingService: ShoppingService = retrofit.create(ShoppingService::class.java) override val categoryRepo: CategoryRepo = NetworkCategoryRepo(shoppingService) override val itemRepo: ItemRepo = NetworkItemRepo(shoppingService) override val userRepo: UserRepo = NetworkUserRepo(shoppingService) } -
@HandstandSam #DevFestFL Network Graph interface NetworkGraph { val categoryRepo: CategoryRepo
val itemRepo: ItemRepo val userRepo: UserRepo } class NetworkGraphImpl( networkConfig: NetworkConfig ) : NetworkGraph { private val retrofit: Retrofit= Retrofit.Builder() .baseUrl(networkConfig.fullUrl) .addConverterFactory(moshiConverterFactory) .addCallAdapterFactory(rxJava2CallAdapterFactory) .client(okHttpClientBuilder.build()) .build() private val shoppingService: ShoppingService = retrofit.create(ShoppingService::class.java) override val categoryRepo: CategoryRepo = NetworkCategoryRepo(shoppingService) override val itemRepo: ItemRepo = NetworkItemRepo(shoppingService) override val userRepo: UserRepo = NetworkUserRepo(shoppingService) } -
@HandstandSam #DevFestFL Create Graphs of Dependencies • Similar to Dagger
@Component OkHttpClient Services Retrofit NetworkGraph Url Repos -
@HandstandSam #DevFestFL Session Graph interface SessionGraph { val sessionManager: SessionManager
val checkoutCart: CheckoutCart val userPreferences: UserPreferences } -
@HandstandSam #DevFestFL Session Graph interface SessionGraph { val sessionManager: SessionManager
val checkoutCart: CheckoutCart val userPreferences: UserPreferences } class SessionGraphImpl( appContext: Context ) : SessionGraph { override val checkoutCart = CheckoutCart() override val userPreferences = UserPreferences(appContext) override val sessionManager = SessionManager(checkoutCart, userPreferences) } -
@HandstandSam #DevFestFL Multiple or Nested Graphs are AppGraph SessionGraph NetworkGraph
! Each module project needs their own graph -
@HandstandSam #DevFestFL Multiple or Nested Graphs are AppGraph SessionGraph NetworkGraph
! Each module project needs their own graph ! Each graph is similar to a Dagger @Component -
@HandstandSam #DevFestFL ! Use the `by lazy` language feature of
Kotlin to delay initialization, but cache result. Leveraging `by lazy` -
@HandstandSam #DevFestFL ! Use the `by lazy` language feature of
Kotlin to delay initialization, but cache result. Leveraging `by lazy` class AppGraph { val networkGraph by lazy NetworkGraph(config = config) } -
@HandstandSam #DevFestFL ! Create a factory with a lambda Factories
class AppGraph { val dbFactory: () -> Database = {Database(logger = logger, utils = utils)} } val db = dbFactory() -
@HandstandSam #DevFestFL ! Use a factory, but this is DIY,
so you will have to manage scoping yourself. Scoping -
@HandstandSam #DevFestFL Setting up Your Graphs class MyApplication : Application
{ val graph by lazy { AppGraph(this) } override fun onCreate() { //Access Graph to Instantiate graph } } ! Create Graph during Application::onCreate() and attach to Application -
@HandstandSam #DevFestFL Setting up Your Graphs class MyApplication : Application
{ val graph by lazy { AppGraph(this) } override fun onCreate() { //Access Graph to Instantiate graph } } class MyApplication : Application { lateinit var graph: AppGraph override fun onCreate() { graph = AppGraph(this) } } or ! Create Graph during Application::onCreate() and attach to Application -
@HandstandSam #DevFestFL Supporting Graphs in Modules class AppGraph(context: Context) {
init { instance = this } companion object { lateinit var instance: AppGraph } } -
@HandstandSam #DevFestFL ! Create a singleton since the module is
unaware of the Application Supporting Graphs in Modules class AppGraph(context: Context) { init { instance = this } companion object { lateinit var instance: AppGraph } } -
@HandstandSam #DevFestFL ! Constructors of Application, Fragments, Activities & Services
are called by the platform Dependency Injection on Activities & Fragments -
@HandstandSam #DevFestFL ! Constructors of Application, Fragments, Activities & Services
are called by the platform ! You have to wait do do any injection until after the `onCreate` lifecycle method Dependency Injection on Activities & Fragments -
@HandstandSam #DevFestFL ! lateinit var, grabbing from Application Accessing Graph
in Activities & Fragments class MyActivity : Activity() { lateinit private var graph: AppGraph override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) graph = (application as MyApplication).graph //Access your fields now graph.database } } -
@HandstandSam #DevFestFL ! Simplify using an extension function. Accessing Graph
in Activities & Fragments class MyActivity : Activity() { lateinit private var graph: AppGraph override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) graph = application.graph } } fun Application.graph(): AppGraph{ return (this as MyApplication).graph } -
@HandstandSam #DevFestFL ! OR use `by lazy` to defer access
until the Activity has been created Accessing Graph in Activities & Fragments class MyActivity : Activity() { private val graph: AppGraph get() = application.graph() private val db: UserRepo by lazy {graph.networkGraph.userRepo} } fun Application.graph(): AppGraph{ return (this as MyApplication).graph } -
@HandstandSam #DevFestFL Accessing Graph in Activities & Fragments class MyActivity
: Activity() { private val graph: AppGraph get() = application.graph() private val db: UserRepo get() = graph.networkGraph.userRepo } fun Application.graph(): AppGraph{ return (this as MyApplication).graph } -
@HandstandSam #DevFestFL ! OR use `get() =` to defer access
until the Activity has been created Accessing Graph in Activities & Fragments class MyActivity : Activity() { private val graph: AppGraph get() = application.graph() private val db: UserRepo get() = graph.networkGraph.userRepo } fun Application.graph(): AppGraph{ return (this as MyApplication).graph } -
@HandstandSam #DevFestFL Pro Tips ! Try and avoid `lateinit var`
for injected values -
@HandstandSam #DevFestFL Pro Tips ! Try and avoid `lateinit var`
for injected values ! `lateinit` can create runtime Exceptions -
@HandstandSam #DevFestFL Pro Tips ! Try and avoid `lateinit var`
for injected values ! `lateinit` can create runtime Exceptions ! `var` creates mutable state -
@HandstandSam #DevFestFL Pro Tips ! Try and avoid `lateinit var`
for injected values ! `lateinit` can create runtime Exceptions ! `var` creates mutable state ! `by lazy` create an anonymous class -
@HandstandSam #DevFestFL Pro Tips ! Try and avoid `lateinit var`
for injected values ! `lateinit` can create runtime Exceptions ! `var` creates mutable state ! `by lazy` create an anonymous class ! You can use `get() =` if your data is an immutable singleton -
@HandstandSam #DevFestFL Accessing Dependencies from the Graph LoginPresenter( view =
loginView, sessionManager = appGraph.sessionGraph.sessionManager, userRepo = appGraph.networkGraph.userRepo ) -
@HandstandSam #DevFestFL Accessing Dependencies from the Graph private val sessionManager:
SessionManager get() = graph.sessionGraph.sessionManager private val userRepo: UserRepo get() = graph.networkGraph.userRepo LoginPresenter( view = loginView, sessionManager = sessionManager, userRepo = userRepo ) -
@HandstandSam #DevFestFL Key Takeaways ! You don’t HAVE to use
a Dependency Injection Library, Dagger -
@HandstandSam #DevFestFL Key Takeaways ! You don’t HAVE to use
a Dependency Injection Library, Dagger ! There are no magic annotations. @Inject -
@HandstandSam #DevFestFL Key Takeaways ! You don’t HAVE to use
a Dependency Injection Library, Dagger ! There are no magic annotations. @Inject ! Kotlin language features make this concise and straightforward -
@HandstandSam #DevFestFL Key Takeaways ! You don’t HAVE to use
a Dependency Injection Library, Dagger ! There are no magic annotations. @Inject ! Kotlin language features make this concise and straightforward ! Dependency Injection is critical for any app -
@HandstandSam #DevFestFL Thank you. My team is hiring in DC,
Richmond & San Francisco.