Dependency Injection

Ken Baldauf

Senior Software Engineer @ Acorns

What is dependency injection?

  • A technique for supplying dependencies to an object 
    • Client: receiving object
    • Service: injected dependency
      • Often times abstracted behind an interface
    • Injector: code that gives the service to the client
  • Service is passed to client; not built or fetched by client
    • Client knows how to use service; not how to create it
  • A form/subset of inversion of control
    • Objects do not create the objects they depend on
    • Needed objects are gotten from external source

Why use dependency injection?

  • Promotes a separation of concerns
    • Separates construction & use of an object
  • Interface between client & service facilitates decoupling
    • Allows client's behavior to be fixed
    • While implementation of the service can change
    • Ex: A client that depends on a analytics SDK
      • Firebase & Segment wrapped in the same interface
      • Client doesn't care if which analytics SDK is used
      • Injector is responsible for choosing which to inject
  • Improves code's testability, reusability & readability
  • Dependencies can be resolved at runtime vs compile time 

Testability

  • Client injected with mocked/stubbed dependencies
  • Loosely coupled code is easier to unit test
    • Allows better isolation of code being tested
  • Simple to mock static methods
    • Wrap static methods in interface
    • Inject implementation of interface into client
class Service() : ServiceInterface {
  fun doStatic() {
    StaticClass.doStuff()
  }
}

interface ServiceInterface {
  fun doStatic()
}
class DIClient(service: ServiceInterface) {
  init {
    service.doStatic()
  }
}

class Client() {
  init {
    StaticClass.doStuff()
  }
}

Why not use dependency injection?

  • Compile time errors are pushed to runtime
  • If unfamiliar, can be difficult to learn
  • Hinders IDE tools
    • Ex: find references
  • Initial setup poses an upfront cost
  • Can become a crutch if overused
  • With some DI framework
    • Error messages may be misleading
    • Minor runtime or compile time performance cost

Types of dependency injection

  • Constructor injection
    • Services provided via client's constructor
  • Setter injection
    • Client exposes setter method for receiving services
  • Interface injection
    • Injector method that will inject the service into any client passed into it
    • Client implements an interface that exposes a setter method that accepts the service
  • Some frameworks use reflection and/or code generation to facilitate field injection

Constructor injection

class Client constuctor(
  val service: Service
) { }
  • Preferred if all dependencies can be constructed first
  • Ensures client is always in a valid state
    • Never missing dependencies
  • Dependencies are immutable
  • Lacks flexibility

Setter injection

class Client() {
  private lateinit var service: Service
  
  fun setService(service: Service) {
    this.service = service
  }
}
  • Requires setter method for each dependency
  • Freedom to update dependencies at any time
  • Difficult to determine if all dependencies have been set

Interface injection

interface ServiceSetter {
  fun setService(service: Service)
}

class Client() : ServiceSetter {
  private lateinit var service: Service
  
  override fun setService(service: Service) {
    this.service = service
  }
}

class ServiceInjector() {
  fun inject(client: ServiceSetter) {
    client.setService(Service())
  }
}
  • Similar to setter injection
  • Dependencies can be injected while remaining ignorant of their clients

Service locator pattern

  • Service locator: acts as a central registry
    • Know how to return dependencies when requested
  • Similar to DI in that it promotes decoupling
    • Client is independent of service's implementation
  • Difference between DI & service locator pattern
    • DI has no explicit request for any given dependency
    • Client asks the locator for the service explicitly
  • All clients have a dependency on the locator
  • Simpler than DI, but some consider it an anti-pattern
  • In Android, many DI frameworks are actually service locators & not sure dependency injection

Android dependency injection

  • Gained popularity due in part to DI's popularity in Java
  • Popular frameworks for Android apps
    • Dagger 2
    • Koin
    • Kodein
  • Alternative frameworks for Android apps
    • Toothpick
    • Katana
      • Deprecated with the release of Hilt
    • Transfuse
      • Goes beyond traditional DI
      • Transforms how you interact with Android SDK

Dagger 2

  • Most popular Android DI framework
  • Compile time DI framework for Java
  • Doesn't use reflection
  • Heavy use of annotation processing
  • Lots of generated boilerplate; overly complex
    • Heavy learning curve compared to alternative options
  • Maintained by Google
    • Based of Square's Dagger 1
  • Hilt: opinionated Dagger extension
    • Standardizes the way Dagger is incorporated into an Android project
  • Dagger is a service locator with generated DI code

Dagger 2

@Module
interface AppModule {

  @Binds
  fun provideService(service: ServiceImpl): Service
}

@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {

  fun inject(activity: MainActivity)
}


class MyApplication : Application() {

  val appComponent: AppComponent by lazy {
    DaggerAppComponent.create()
  }
}

class MainActivity : Activity() {

  @Inject
  lateinit var service: Service
  
  fun onCreate(savedInstanceState: Bundle?) {
    (application as MyApplication).appComponent.inject(this)
  }
}

Dagger 2 with Hilt

@HiltAndroidApp
class MyApplication : Application() {

}

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

  @Inject
  lateinit var service: Service
  
}

Koin

  • 2nd most popular framework on Android
  • Lightweight Kotlin dependency injection framework
  • Lazily resolves dependencies at runtime
    • No compile time safety or error
  • Leverages a Kotlin DSL (domain-specific language)
  • Doesn't use code generation or reflection
    • Slight runtime performance hit
    • Faster compilation due to no code generation
  • Less of a learning curve than Dagger
  • Not true DI; uses service locator pattern

Koin

val appModule = module {
  single<Service> { ServiceImpl() }
}

class MyApplication : Application() {

  override fun onCreate() {
    super.onCreate()

    startKoin {
      modules(appModule)
    }
  }
}

class MainActivity : Activity() {

  private val service by inject<Service>()
}

Kodein

  • KOtilin DEpendency INjection
  • Designed to provide painless DI in Kotlin projects
  • Lazily resolves dependencies at runtime
    • No compile time safety or error
  • Similar to Koin, Kodein leverages a Kotlin DSL
  • Uses Kotlin inline functions under the hood
  • Doesn't use any code generation
    • Slowest runtime performance
  • Similar to Koin, but community is roughly 1/3rd the size
    • Documentation isn't as up to date
  • Less of a learning curve than Dagger
  • Not true DI; uses service locator pattern

Kodein

val appModule = Kodein.Module(name = "appModule") {
  bind<Service>() with singleton { Service() }
}

class MyApplication : Application(), KodeinAware {

  override val kodein = Kodein.lazy {
    import(appModule)
  }
}

class MainActivity : Activity(), KodeinAware {

  override val kodein by closestKodein()

  private val service by instance<Service>()
}

So what's next?

DI Deep Dive with Dagger

Wednesday, April 28th @ 6:30pm

Questions?

Made with Slides.com