Dagger is a fully static and compile-time dependency injection framework. Compile-time means that issues in the dependency graph (such as cycles or missing providers) are caught during build-time.
Dagger creates the dependency graph using components and subcomponents
- Components are top-level containers of providers that are pulled from modules that component is configured to include
- Subcomponents are also containers, and may contain other subcomponents
- Subcomponents automatically inherit all the dependencies from their parent components
- Components/subcomponents can automatically collect dependencies for which they are scoped
Scopes are compile-time annotations associated both with a component/subcomponent and either injectable objects or providers of objects
Dagger supports two types of injections: field and constructors
- All objects that are injected need to have an @Inject-declared constructor
- Any parameters passed into an @Inject-declared constructor will be retrieved from the dependency graph
- Fields can be marked as @Inject-able, but a separate inject() method in a component needs to be added for that class to initialize those fields, and the class must call this method
Note: Classes can have their providers inferred just by being qualified and having an @Inject-able constructor--no need for a Dagger module
Dagger modules are defined in separate classes annotated with the @Module tag
- Modules can provide an implementation with @Provides
- Modules can bind one type to another type using @Binds
- Dagger object lifetimes need to be compatible with Android object lifecycles
- Prefer constructor injection over field injection to encourage encapsulation
- Result are activity/fragment/view presenter classes that are field-injected into their corresponding Android objects, but themselves support constructor injection
You can understand it with this example :
This is a Singleton-scoped object with dependency. Note that because Factory is @Singleton
scoped, it can inject everything in the Singleton component including blocking dispatcher.
@Singleton
class Factory @Inject constructor(@BlockingDispatcher private val blockingDispatcher: CoroutineDispatcher) {
fun <T: Any> create(): InMemoryBlockingCache<T> {
return InMemoryBlockingCache(blockingDispatcher)
}
}
These are Singleton-scoped providers with custom qualifiers. Note also that to distinguish between two of the same types, we can use custom qualifier annotations like @BackgroundDispatcher and @BlockingDispatcher.
@Module
class DispatcherModule {
@Provides
@BackgroundDispatcher
@Singleton
fun provideBackgroundDispatcher(): CoroutineDispatcher {
return Executors.newFixedThreadPool(4).asCoroutineDispatcher()
}
@Provides
@BlockingDispatcher
@Singleton
fun provideBlockingDispatcher(): CoroutineDispatcher {
return Executors.newSingleThreadExecutor().asCoroutineDispatcher()
}
}
- Dependencies can be replaced at test time
- This is especially useful for API endpoints! We can replace Retrofit instances with mocks that let us carefully control request/response pairs
- This is also useful for threading! We can synchronize coroutines and ensure they complete before continuing test operations
- Tests can declare their own scoped modules in-file
- Tests themselves create a test application component and inject dependencies directly into @Inject-able fields
- Bazel (#59) will make this even easier since test modules could then be shareable across tests
Here is an example of testing with Oppia Dagger. This shows setting up a test component and using it to inject dependencies for testing purposes. It also shows how to create a test-specific dependency that can be injected into a test for manipulation.
class InMemoryBlockingCacheTest {
@field:[Inject TestDispatcher] lateinit var testDispatcher: TestCoroutineDispatcher
private val backgroundTestCoroutineScope by lazy { CoroutineScope(backgroundTestCoroutineDispatcher) }
private val backgroundTestCoroutineDispatcher by lazy { TestCoroutineDispatcher() }
@Before fun setUp() { setUpTestApplicationComponent() }
@Test fun `test with testDispatcher since it's connected to the blocking dispatcher`() = runBlockingTest(testDispatcher) { /* ... */ }
private fun setUpTestApplicationComponent() {
DaggerInMemoryBlockingCacheTest_TestApplicationComponent.builder().setApplication(ApplicationProvider.getApplicationContext()).build().inject(this)
}
@Qualifier annotation class TestDispatcher
@Module
class TestModule {
@Singleton @Provides @TestDispatcher fun provideTestDispatcher(): TestCoroutineDispatcher { return TestCoroutineDispatcher() }
@Singleton @Provides @BlockingDispatcher
fun provideBlockingDispatcher(@TestDispatcher testDispatcher: TestCoroutineDispatcher): CoroutineDispatcher { return testDispatcher }
}
@Singleton
@Component(modules = [TestModule::class])
interface TestApplicationComponent {
@Component.Builder interface Builder { @BindsInstance fun setApplication(application: Application): Builder fun build(): TestApplicationComponent }
fun inject(inMemoryBlockingCacheTest: InMemoryBlockingCacheTest)
}
}
Dagger compile-time errors can be hard to understand
- When you encounter one: scan the error for the dependency name (it's likely a dependency you just imported into the file failing to compile)
- Search for the Dagger module you want to use to provide that dependency
- Make sure your Gradle module or Bazel build file depends on the library that contains the module you need
- Note that Gradle modules cannot depend on the app module, which means any Dagger modules in the app Gradle module are inaccessible outside of the app module