DEV Community

Cover image for The dagger.android Missing Documentation
Tony Robalik
Tony Robalik

Posted on • Edited on

The dagger.android Missing Documentation

No thermosiphons here!

  1. This part
  2. Part 2 — ViewModels and View Model Factories
  3. Part 3 — Fragments

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?

Finding thermosiphons

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"
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's what we've done:

  1. Defined a custom Application,
  2. that implements HasActivityInjector,
  3. that defines the root/global/singleton/app component that is the parent of all of your app's subcomponents,
  4. that specifies that it takes an instance of the application itself (@BindsInstance), which means that we now have our MainApplication instance available to this component and all its subcomponents.
  5. 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?

magic

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
}
Enter fullscreen mode Exit fullscreen mode

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 Activitys

// 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()))
  }
}
Enter fullscreen mode Exit fullscreen mode

passing test

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 Views! (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

  1. Basic setup (this post)
  2. Using Dagger with ViewModels and LiveData
  3. More to come...

Resources

  1. All the code here is available on my Github repo
  2. Keeping the Daggers Sharp, which taught me something about scopes
  3. 5-part series by Android Dialogs with Pierre-Yves Ricau on Youtube, which explained @BindsInstance, static provision methods, scoping, and so much more.

coffee

Top comments (2)

Collapse
 
pabiforbes profile image
Pabi Forbes

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.

Collapse
 
autonomousapps profile image
Tony Robalik

Thank you! I have more posts planned to continue the series, and I hope you find those useful, as well.