-
Notifications
You must be signed in to change notification settings - Fork 520
Dagger
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
There's an Android-specific dependency hierarchy:
|
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
Have an idea for how to improve the wiki? Please help make our documentation better by following our instructions for contributing to the wiki.
Core documentation
Developing Oppia
- Contributing to Oppia Android
- Bazel
- Key Workflows
- Testing
- Developing Skills
- Frequent Errors and Solutions
- RTL Guidelines
- Working on UI
- Writing Design Docs
Developer Reference
- Code style
- Background Processing
- Dark mode
- Buf Guide
- Firebase Console Guide
- Platform Parameters & Feature Flags
- Work Manager
- Dependency Injection with Dagger
- Revert & regression policy
- Upgrading target SDK version
- Spotlight Guide
- Triaging Process
- Bazel
- Internationalization
- Terminology in Oppia
- Past Events