Painless Dagger+Android+Kotlin

Historically the words “Dagger” and “painless” have been mutually exclusive. Dagger has often been associated with complex configuration. Subsequent alternative Kotlin based frameworks have popped up with slogans: “Painless Kotlin Dependency Injection” and “A pragmatic lightweight dependency injection framework for Kotlin developers”. Unfortunately, there is no free lunch and the alternative Kotlin based solutions introduce a different set of tradeoffs such as compile time safety and runtime performance. Why can’t we have our cake and eat it too?
In this article, we will evaluate a simple Kotlin, Android, Dagger app minus the boilerplate and magic. Unfortunately, the official documentation is less than helpful. As an Android developer, I’m a lot more familiar with Activities and Fragments than Themosiphons and Heaters. We will not be covering what or why of dependency injection which has been covered elsewhere.
Key Takeaways
- You probably don’t need dagger-android or multibindings
- FragmentFactory and ViewModelProvider.Factory allow constructor injection
- Modules are only required for interfaces and third-party classes
- Component is only needed to obtain things directly from Dagger
- You don’t need to create any additional Dagger configuration for unit tests
Setup Gradle
First things first. The appropriate dependencies and plugins need to added to Gradle.
Fragment
The Fragment is instantiated with a ViewModel and associated with a view. The Fragment has a single dependency injected, custom ViewModelProvider.Factory. Since the Fragment is annotated with @Inject constructor, Dagger does not require any additional configuration to be able to manage this class.
The Fragment’s sole job is to maintain the view and respond to user interactions. To accomplish this it will subscribe to appropriate LiveData observables from ViewModel.
ViewModel and Service
Similar to the Fragment the ViewModel and Service are both utilize @Inject constructor, enabling Dagger to create the dependency graph with no additional setup. In a real application, the service can specify additional dependencies such as Retrofit or Room.
FragmentFactory is responsible for creating new instances of Fragments and injecting the associated graph in. Each Fragment that the factory is responsible for creating needs to be injected into the factory via constructor. The Android framework will utilize the fragment factory to create instances of the Fragments on demand using the instantiate method.
To delay the creation of the Fragment until requested by instantiate we utilize the Provider wrapper. Injecting the Provider rather than Fragment directly will delay the creation of the Fragment and associated dependencies until Provider.get() is called from instantiate method. The newly created Fragment will adhere to the Android Fragment lifecycle we all know and love.
ViewModelProvider.Factory follows the same pattern as the FragmentFactory above for ViewModel creation. It will be responsible for creating and managing the associated AAC ViewModels. The ViewModel will be bound to the Fragment during the onCreate lifecycle and new instances will be created as required using the create method with injected dependencies.
Provider is an interface defined in JSR-330. By relying on standard interfaces, we can avoid any dependencies on Dagger or other DI frameworks. We can easily swap to an alternative DI framework, create our own or manually construct the graph for unit tests.
An alternative method to specifying each Provider dependency in constructor is Dagger multibindings. Multibindings move the complexity from the Factory into Dagger modules which has introduces its own set of tradeoffs. Multibindings are counterproductive to our goal of reducing complexity and thus won’t be covered.
Glue: Dagger
Dagger Module
So far all of the code has been plain Android code required to load a view and display data. To actually make the application work though, we need to create some glue code to tie the factories, viewmodels, services, and fragments together. We can either write all of the glue code by hand manually or just let Dagger do the heavy lifting. I’m not sure about you but I’d rather work on new features than create more DI boilerplate by hand. Since the Dagger code is completely independent of the implementation, during testing, we can wire things together manually for unit tests or create a new Dagger component for integration testing.
Since most of the classes were annotated with @Inject constructor annotations Dagger is able to automatically create the dependency without any additional configuration. FragmentFactory and ViewModelProvider.Factory are interfaces though so Dagger will need some guidance on which implementation to use in the Module.
Glue: Dagger Component
If the modules describe the “how”, the components describe the “what”. In our app a single Activity will create multiple Fragments, ViewModels and transitive dependencies. Given the FragmentFactory as root, Dagger will generate all factory boilerplate to generate all of the additional dependencies required. We include a reference to the associated Module created above.
Activity: Putting it all together
Since the introduction of Jetpack, Google has recommended a Single Activity architecture which provides a convenient single entry point to application to create Fragments and the rest of the dependency graph. The Activity will be responsible for initializing Dagger and inserting the Fragment factory and Dagger will handle the rest. That’s it, we’re done.
But what if I want my dependencies to outlast the Activity lifecyle?
Certain dependencies may be expensive to create or want to survive at the application scope rather than Activity such as Room or Retrofit instances. In this case they can be annotated with @Singleton at module scope and the Dagger graph can be managed by Application
Then we just need to initialize the Dagger graph in the Application
What if one of the dependencies needs Context such as Room?
Simply specify application context as constructor in module dependency.
Then just pass in Application context into module as part of builder
What about testing UI testing?
Since everything is decoupled and dependencies are injected in via constructor, we have the option of creating our own glue for each test. No additional Dagger config is required for testing. With the release of androidx.test Google has provided a common API for both JVM (Robolectric) and Instrumented (Espresso) testing. We can easily create Fake factories that enable us to inject mocked instances to test Fragments in isolation.
Now we can easily create a JVM Robolectric or Instrumented Espresso test to verify the Fragment in isolation. We are only interested in verifying the functionality of the Fragment (View) so we mock out the ViewModel.
Sources
Complete Project on Github