No thermosiphons here!
So you want to use dagger.android, Dagger2's (relatively) new package aimed specifically at you, The Android Developer, but are confused by the lack of thermosiphons and coffee makers in the official documentation. What's a dev to do?
I've got you covered. This is a continuation of my series on rewriting Chess.com's Android app. I try to be thorough, but if it turns out I skimmed over something, please let me know in the comments.
Let's get started!
Project setup
Add the following to your app/build.gradle
file
// If you're using Kotlin
apply plugin: 'kotlin-kapt'
dependencies {
// ...all the libs...
// Dagger
def dagger_version = "2.15"
// Required
implementation "com.google.dagger:dagger-android:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
// Required if you use anything prefixed with AppCompat or from the support library
implementation "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
// Required if you care about testing, and of course you care about testing. Required.
kaptAndroidTest "com.google.dagger:dagger-compiler:$dagger_version"
kaptAndroidTest "com.google.dagger:dagger-android-processor:$dagger_version"
}
Injecting your custom Application
Create a custom Application class, call it (say) MainApplication
, and make it look like this:
// open because we will have a DebugMainApplication for testing
open class MainApplication : Application(), HasActivityInjector {
// Required by HasActivityInjector, and injected below
@Inject
protected lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun activityInjector() = dispatchingAndroidInjector
override fun onCreate() {
super.onCreate()
initDagger()
}
private fun initDagger() {
DaggerMainApplication_MainApplicationComponent.builder()
.app(this)
.build()
.inject(this)
}
// Doesn't need to be a nested class. I could also put this in its own file or,
// this being Kotlin, in the same file but at the top level.
@Singleton
@Component(modules = [
// provided by dagger.android, necessary for injecting framework classes
AndroidSupportInjectionModule::class,
// Defines a series of Subcomponents that bind "screens" (Activities, etc)
ScreenBindingModule::class,
])
interface MainApplicationComponent {
fun inject(app: MainApplication)
@Component.Builder
interface Builder {
fun build(): MainApplicationComponent
@BindsInstance fun app(app: Context): Builder
}
}
}
Here's what we've done:
- Defined a custom
Application
, - that implements
HasActivityInjector
, - that defines the root/global/singleton/app component that is the parent of all of your app's subcomponents,
- that specifies that it takes an instance of the application itself (
@BindsInstance
), which means that we now have ourMainApplication
instance available to this component and all its subcomponents. - And finally, built Dagger's generated implementation of that component's contract, and then used it to inject our Application with a
DispatchingAndroidInjector<Activity>
What is a DispatchingAndroidInjector
?
It is what ultimately injects your framework class (Activities, Fragments, Services, etc). Later on, in your Activities, you'll be calling AndroidInjection.inject(this)
, and this makes use of the DispatchingAndroidInjector
instance that your MainApplication
provides via HasActivityInjector
.
ScreenBindingModule
// Don't worry, it'll get bigger!
@Module abstract class ScreenBindingModule {
@ActivityScoped // optional
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun mainActivity(): MainActivity
}
ScreenBindingModule
is an abstract class annotated with @dagger.Module
. In it, we need to add an abstract function annotated with @ContributesAndroidInjector
for each Activity we want to inject. This function should return an instance of the activity (it doesn't actually "create" your activity; this is just how Dagger knows which class is being injected). We can optionally specify modules to install on this subcomponent, and optionally specify scopes. Each of these functions actually defines a Subcomponent.Builder
used to inject your Activity classes; the code itself is generated by dagger. Essentially, you're going to have one function per Activity.
(PS: @ActivityScoped
is a custom scope that you'll have to define yourself. See the full code sample linked below.)
Injecting your Activity
s
// We can define this anywhere we like, but it's convenient to include
// in the same file as the class being injected
// An object because I want to provide a static function, and it's Kotlin
@Module object MainActivityModule {
// static because dagger can call this method like MainActivityModule.provideText(),
// rather than new MainActivityModule().provideText()
@Provides @JvmStatic fun provideText() = "Why, hello there!"
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var text: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.text = text
}
}
The result, assuming our layout activity_main
has a TextView
named textView
, is a simple screen showing the text "Why, hello there!"
Well that's great and all, but now what? (And where's my coffee?!)
Great questions. This brings me to...
Testing your activity
Let's assume that the string "Why, hello there!"
could be anything; it's generated dynamically; maybe it's provided by a build script or retrieved via an API call. We don't want to rely on any of that in a test environment, and anyway, we have that API code unit-tested (right?). We just want to verify that our screen shows some text, given that the text exists. Here's one way to accomplish that.
First, add a DebugMainApplication
class DebugMainApplication : MainApplication() {
fun setTestComponent(component: MainApplicationComponent) {
component.inject(this)
}
}
This replaces the prod component with our custom test component (see below).
And create a debug variant of your manifest in debug/AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.autonomousapps.daggerdotandroid">
<application
android:name="com.autonomousapps.daggerdotandroid.DebugMainApplication"
android:label="@string/app_name"
tools:ignore="AllowBackup,GoogleAppIndexingWarning"
tools:replace="android:name,android:label"/>
</manifest>
Our test code will now use our special DebugMainApplication
instead of our prod MainApplication
.
Write your test
class MainActivityTest {
// Rules are one place where Kotlin code is uglier and more verbose than Java
@get:Rule private var activityTestRule = ActivityTestRule(
MainActivity::class.java,
false,
false
)
// Here we define a test component that extends our production component
@Component(modules = [
// Inheriting components don't inherit annotations, so we need to re-declare
// that we want to inject framework classes
AndroidSupportInjectionModule::class,
// A test module defined below
TestMainActivityModule::class
])
interface TestMainApplicationComponent : MainApplication.MainApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance fun app(app: MainApplication): Builder
// This is the fruit of all our labor. We can now provide our custom text
// (or anything more interesting!) into our dependency graph
@BindsInstance fun text(text: String): Builder
fun build(): TestMainApplicationComponent
}
}
// This is basically the mirror image of ScreenBindingModule, but instead of
// providing an abstract function for EVERY screen, we only need to provide
// one for the screens that will get injected in our test
@Module abstract class TestMainActivityModule {
@ContributesAndroidInjector abstract fun mainActivity(): MainActivity
}
@Before fun setup() {
val app = InstrumentationRegistry.getTargetContext().applicationContext as DebugMainApplication
val mainComponent = DaggerMainActivityTest_TestMainApplicationComponent.builder()
.app(app)
// Neat!
.text("I'm a test!")
.build()
app.setTestComponent(mainComponent)
activityTestRule.launchActivity(null)
}
@Test fun verifyText() {
onView(withText("I'm a test!")).check(matches(isDisplayed()))
}
}
Cool beans. By the way, if you're confused that we have approximately a bajillion lines of setup code for a test that is really just one line long -- hey, welcome to the wonderful world of Android testing! Join us, it's fun. Also, a real test class would have more than one test and the ratio of boilerplate:test-code should eventually become something non-insane.
What about fragments?
Please keep an eye on this page, because there is more to come! We'll be talking about injecting fragments, special considerations for injecting retained fragments, and we'll even write a custom class for injecting View
s! (View
-injection is not supported out of the box by dagger.android
, for reasons that will become clear.) We'll also see how to incorporate ViewModels, ViewModelProvider.Factorys, and maybe even custom Scopes....
This series
- Basic setup (this post)
- Using Dagger with ViewModels and LiveData
- More to come...
Resources
- All the code here is available on my Github repo
- Keeping the Daggers Sharp, which taught me something about scopes
- 5-part series by Android Dialogs with Pierre-Yves Ricau on Youtube, which explained
@BindsInstance
, static provision methods, scoping, and so much more.
Top comments (2)
Hey Tony, thank you for the post. I like your way of explaining concepts. I haven't used Dagger before but I want to start using it, it does seem quite scary and confusing at first.
Thank you! I have more posts planned to continue the series, and I hope you find those useful, as well.