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?

Dependency Injection

By Kenneth Baldauf

Dependency Injection

What is dependency injection? Why you should use it? What does it do behind the scenes? What options exist for Android applications?

  • 278