27 September 2023
Note: This article assumes some familiarity with Retrofit.
Welcome back to "A Pragmatic Introduction to Dagger on Android". In the last article we discussed why you would
want to use a Dependency Injection framework like Dagger 2. In this article, we're going to see how by configuring
a custom OkHttpClient wired into a Retrofit instance, which is then used to create a TypicodeService. Our
dependency chain will look like the following:
TypicodeService -> Retrofit -> OkHttpClient
In this article, we'll do the following to use Dagger and inject MainActivity with a TypicodeService:
Application classTypicodeService into an ActivityTo start, you'll need to clone this guides code with the following command:
git clone --branch part-2-start https://github.com/danielPerez97/pragmatic-dagger.git
Once you've cloned the code, open it in Android Studio and sync with Gradle.
kaptFirst, we'll need to enable kapt (Kotlin Annotation Processing Tool) in our app-module build.gradle.kts file.
In our plugins{} block, add the following line:
kotlin("kapt")
Sync your IDE with Gradle.
Add these lines to the dependencies {} block of your app-module build.gradle.kts file:
// Dagger - Use the correct version in the banner above for "2.x"
implementation("com.google.dagger:dagger:2.x")
kapt("com.google.dagger:dagger-compiler:2.x")
Sync your IDE with Gradle.
Next up, we're going to create something called a Component. Before we do that though, let's talk about what a Component is and what it's used for.
In the last article, we said this about Dagger:
"Is it actually this hard to set up?"
Yeah, I'd go ahead and say Dagger 2 is a bit hard to set up, but that's partly because it's based on 100% code
generation instead of reflection. We make this tradeoff for major performance gains on mobile devices.
Compared to something like Spring Dependency Injection where it's an out-of-the-box solution, Dagger 2 is often configured manually and requires us to call into the generated code. I still think it's almost as easy to use as possible given this constraint because of Components and Annotations. Almost.*
A Component is just an interface that you define to begin using Dagger's generated code. Components are able to expose dependencies from the Dagger graph directly as well as perform members-injection on other classes. We will define a component for our Android application, use Dagger-specific annotations on it, then take a look at the generated code.
Let's go ahead and define a TypicodeAppComponent in the dev.danperez.typicode example, with nothing
Dagger-specific yet:
// main/java/dev/danperez/typicode/TypicodeAppComponent.kt
package dev.danperez.typicode
interface TypicodeAppComponent
{
fun inject(mainActivity: MainActivity)
}
We're going to be using Members Injection on MainActivity, so the fun inject(mainActivity: MainActivity)
function is about as simple as we can make it. We won't provide the implementation for TypicodeAppComponent; instead,
Dagger will generate it for us.
To make the Dagger Compiler aware of our TypicodeAppComponent, all we have to do is add the @dagger.Component
and @javax.inject.Singleton annotations:
// main/java/dev/danperez/typicode/TypicodeAppComponent.kt
package dev.danperez.typicode
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component
interface TypicodeAppComponent
{
fun inject(mainActivity: MainActivity)
}
@Singleton is an annotation we will use for a feature called scopes. We can use this later to tell Dagger NOT to
create expensive objects over and over again, but to cache it with the lifecycle of the Component.
In order for Dagger to begin generating code, we'll need to build the app module. You can use the hammer icon at the top of Android Studio's New UI to do this or use the following command:
./gradlew :app:clean :app:assembleDebug
Note: For this command to work, your JAVA_HOME environment variable MUST point to at least a Java 11 JDK.
This will generate DaggerTypicodeAppComponent at
/app/build/generated/source/kapt/debug/dev/danperez/typicode/DaggerTypicodeAppComponent.java. You can also view it
below.
Note: Up to this point, all of our code has been in Kotlin. Dagger generates Java code, so be prepared to read Java code if you're ever looking at what Dagger generates.
// Generated by Dagger (https://dagger.dev).
package dev.danperez.typicode;
import dagger.internal.DaggerGenerated;
@DaggerGenerated
@SuppressWarnings({
"unchecked",
"rawtypes",
"KotlinInternal",
"KotlinInternalInJava"
})
public final class DaggerTypicodeAppComponent {
private DaggerTypicodeAppComponent() {
}
public static Builder builder() {
return new Builder();
}
public static TypicodeAppComponent create() {
return new Builder().build();
}
public static final class Builder {
private Builder() {
}
public TypicodeAppComponent build() {
return new TypicodeAppComponentImpl();
}
}
private static final class TypicodeAppComponentImpl implements TypicodeAppComponent {
private final TypicodeAppComponentImpl typicodeAppComponentImpl = this;
private TypicodeAppComponentImpl() {
}
@Override
public void inject(MainActivity mainActivity) {
}
}
}
This isn't doing anything yet, but it might be useful to see how this changes later. Note that it provides a static
create()method we can use to retrieve our TypicodeAppComponent. It also has an internal, private
TypicodeAppComponentImpl, but it's constructor and inject(MainActivity mainActivity) don't have anything in
their bodies.
Nonetheless, we'll go ahead and configure this in a custom Application class where we can retrieve it from Activities, Fragments, Services, etc.
Application ClassThe first time I ever saw a custom application class was in the context of learning Dagger, so I'm going to assume this is your first time seeing it too.
A custom application class is a class the Android OS will set up for you in place of the default
android.app.Application class. It's similar to an activity in that you declare it in the manifest and has similar
lifecycle methods(onCreate(), onDestroy(), etc.) you can hook into. Your custom Application class will be created
before any of the 4 main Android OS components(Activities, BroadcastReceivers, ContentProviders, Services) and will be
destroyed after.
First, create a new file called TypicodeApplication.kt in the dev.danperez.typicode package and use the following
code:
package dev.danperez.typicode
import android.app.Application
class TypicodeApplication: Application()
{
lateinit var typicodeAppComponent: TypicodeAppComponent
override fun onCreate() {
super.onCreate()
typicodeAppComponent = DaggerTypicodeAppComponent.create()
}
}
Then, to instruct the Android OS to use your custom application class instead of the default one, simply declare a
name attribute on the <application> tag in your AndroidManifest.xml:
<application
...
android:name=".TypicodeApplication"
...
>
</application>
Now, from our MainActivity, we'll be able to look up our TypicodeAppComponent to retrieve dependencies from the
Dagger graph. Let's go ahead and try to inject a TypicodeService by adding the following lines to MainActivity:
...
import javax.inject.Inject// <-- ADD THIS
...
class MainActivity : ComponentActivity() {
@Inject lateinit var typicodeService: TypicodeService // <-- ADD THIS
var text by mutableStateOf("Hello!")
...
}
Try and build the app module like we did before. You should see a problem:
[Dagger/MissingBinding] dev.danperez.typicode.TypicodeService cannot be provided without an @Provides-annotated method.
public abstract interface TypicodeAppComponent {
^
Missing binding usage:
dev.danperez.typicode.TypicodeService is injected at
dev.danperez.typicode.MainActivity.typicodeService
dev.danperez.typicode.MainActivity is injected at
dev.danperez.typicode.TypicodeAppComponent.inject(dev.danperez.typicode.MainActivity)
[Dagger/MissingBinding] is a well-known error, and it should immediately tell you one thing: Dagger could not find
an object on its graph. This error is saying we're missing a binding for TypicodeService which is supposed to be
injected at MainActivity. In this case, it's because we never told Dagger to put TypicodeService on the graph.
Let's fix that.
Let's revisit our Dependency chain for TypicodeService:
TypicodeService -> Retrofit -> OkHttpClient
TypicodeService is another interface we will not be generating, Retrofit will. The difference between how Dagger and Retrofit does this is simple: Dagger uses code generation at compile-time, Retrofit uses reflection at runtime. For us, this means we now have 2 interfaces whose implementation we won't be writing out by hand.
Since TypicodeService has a dependency on Retrofit, we will need to tell Dagger how to use Retrofit to create
TypicodeService. For classes and implementations whose code we cannot manually modify, we have to use Dagger modules.
Dagger Modules are the mechanism to say "hey Dagger, here's something I'd like for you to stick on your graph. When you retrieve this type for me, here's the code you'll need to run in order to retrieve it."
Let's create a brand new package called di at dev.danperez.typicode.di. In this package, create
TypicodeModule.kt with the following code:
package dev.danperez.typicode.di
import dagger.Module
import dagger.Provides
import dev.danperez.typicode.TypicodeService
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import javax.inject.Singleton
@Module
class TypicodeModule
{
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient
{
return OkHttpClient()
}
@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit
{
return Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
}
@Provides
fun provideTypicodeService(retrofit: Retrofit): TypicodeService
{
return retrofit.create(TypicodeService::class.java)
}
}
Here we're using two new annotations, @Module and @Provides. If we look at the source code(CMD + B or CTRL + B) for
these, we'll see these comments on them from the Dagger 2 maintainers:
@Module: "Annotates a class that contributes to the object graph."
@Provides: "Annotates methods of a module to create a provider method binding. The method's return type is bound
to its returned value. The component implementation will pass dependencies to the method as parameters."
Essentially, @Module tells Dagger "Here's a class/interface that's going to provide some dependencies for your
graph." and @Provides tells Dagger "Here is a specific dependency to add to your graph." The two go hand in hand
very often.
Knowing this, we can quickly tell that provideOkHttpClient() binds an OkHttpClient type onto the Dagger graph.
provideRetrofit(client: OkHttpClient) deserves a bit more attention though.
provideRetrofit(client: OkHttpClient) actually uses the Dagger graph to request an OkHttpClient type. This way,
we can use our OkHttpClient binding multiple times to create different services throughout our Dagger code (or inject
it into an Activity). If OkHttpClient wasn't on the graph already, we would have gotten another [MissingBinding]
error. And as a bonus, @Singleton combined with @Provides tells Dagger, "Create this once and re-use it
everywhere on the graph." Effectively, this is enforcing the Singleton pattern for us in Dagger's generated
code.(It's also good practice to use one OkHttpClient for your entire app too)
The final thing we have to do is edit the @Component annotation on TypicodeAppComponentto the following:
@Component(modules = [TypicodeModule::class])
This "installs" our module into the TypicodeAppComponent. We MUST do this because an application can have multiple
Components, and we have to specify which modules belong to which components.
Finally, we can attempt to inject MainActivity. Go ahead and build your app module again.
Now that we're past the compile error, we just have to actually inject MainActivity by calling Dagger's
generated code. We're going to use a very common method to retrieve our Dagger Component: cast the
applicationContext to our custom Application class, retrieve the Component, and call the inject() method. In
our case, it looks like this:
(applicationContext as TypicodeApplication).typicodeAppComponent.inject(this)
Call this code before super.onCreate() in MainActivity:
package dev.danperez.typicode
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import javax.inject.Inject
class MainActivity : AppCompatActivity() {
@Inject lateinit var typicodeService: TypicodeService
...
override fun onCreate(savedInstanceState: Bundle?) {
(application as TypicodeApplication).typicodeAppComponent.inject(this) // INJECTION
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// This is a coroutine that runs in the lifecycle scope of the activity. It's used to perform asynchronous tasks,
// like network requests, off the main thread. Here, we're using it to test our injected TypicodeService by
// requesting a post from an API and updating the 'text' state with the response.
lifecycleScope.launch {
text = typicodeService.getPost(1).string()
}
}
...
}
Now, you should be able to use TypicodeService throughout MainActivity. In the above code, I've provided some code
that should display the result of the network request in the UI(it should just be a JSON String).
What have we achieved in this article?
MissingBinding errorThat's quite a bit to digest for one article, but I believe that Dagger pays off this upfront complexity with its early failures and runtime speed. Perhaps at a smaller scale, all this setup isn't worth it and Koin might be a better fit for your project. However, if your project consists of many thousands of lines of code and many, many modules, Dagger 2 is probably your best choice for validating your dependency graph at compile-time as well as its speed.
Stay tuned for Part 3 where we'll discuss how to use the Factory pattern to inject Android ViewModel's.
By this, I mean using Dagger + Anvil to automatically install Modules into the Component. I wish it was built into Dagger itself.
— Daniel Perez