Chapters

Hide chapters

Dagger by Tutorials

First Edition - Early Access 1 · Android 11 · Kotlin 1.4 · AS 4.1

Section III: Components & Scope Management

Section 3: 3 chapters
Show chapters Hide chapters

Section IV: Advanced Dagger

Section 4: 3 chapters
Show chapters Hide chapters

Section V: Introducing Hilt

Section 5: 5 chapters
Show chapters Hide chapters

6. Hello, Dagger
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the first section of this book, you did a lot of work to understand some fundamental concepts — and added many lines of code to the Busso App in the process. You started with the concept of dependency. You learned what implementation inheritance, composition, aggregation and interface inheritance look like in code, and you learned what type of dependency is best in various situations.

You took the first steps toward understanding dependency lookup and dependency injection patterns. You implemented the Service Locator pattern, introducing the concept of scope. Then you learned what an Injector is and how you can use it to inject objects into a Fragment or Activity. Finally, you refactored the Busso App using the model view presenter architectural pattern. Now, Busso is much easier to test and simpler to change.

Yes. it’s been a lot of work! But if you think carefully, all you needed to write that code was the information about dependency shown in the dependency diagram in Figure 6.1:

Figure 6.1 — Example of Dependency Diagram
Figure 6.1 — Example of Dependency Diagram

You might wonder, then, if there’s a way to generate all that code automatically. Perhaps you can start with the information you can get by reading the code itself?

Going back to an early example from Chapter 3, “Dependency Injection”, consider this code:

class Server() {

  lateinit var repository: Repository

  fun receive(data: Date) {
    repository.save(date)
  }
}

Is there a way to generate all the code to inject an instance of the Repository implementation into the Server? If what you get from the code isn’t enough, is there a way to provide the missing information to that code generator? The answer is yes: Dagger!

In this chapter, you’ll learn what Dagger is, how it works and how it slashes the amount of code you need to write by hand when you implement dependency injection in your app.

What is Dagger?

Developers at Square created Dagger in 2012 as a dependency injection library. Dagger is a code generation tool.

Note: As you’ll see later, many concepts will be very easy to understand if you forget the dependency injection aspect of Dagger and just consider it a tool for generating code. Dagger is smart enough to get some information from the code itself. In other cases, it’ll need your help.

Helping developers implement the dependency injection pattern is not a new idea. Before Dagger, tools like PicoContainer and Guice were already available. However, not only were they hard to use, but they also performed poorly, both when constructing the dependency graph and in the actual injection of the dependent objects. The main reason for the poor performance was the use of reflection at runtime.

Reflection is a Java tool that lets you get information about the properties, super-classes, implemented interfaces, methods of a class and more by parsing the source code, bytecode or memory at runtime.

Reflection has a big problem: performance. Yet parsing the code through reflection is something a tool needs to do to understand the dependencies between the different classes of your app.

Square had the great idea of moving the code parsing before the compilation task by using an annotation processor. The goal of this code generation task is to create the code you execute to achieve dependency injection.

Understanding the build process

As you know, Java and Kotlin are both compiled languages. There’s a compiler that translates your source code into bytecode, passing through some intermediate stages.

Figure 6.2 — The Build task in Gradle
Yopeki 7.6 — Qxi Reitv bobk ax Ygapfe

> Task :compileKotlin
> Task :compileJava
> Task :classes
> Task :jar
> Task :assemble
> Task :build

Using an annotation processor

An annotation processor is a plug-in that lets you extend the build process of a project. adding some tasks for:

Building the “RayDi” DI framework

To demonstrate how the annotation processor works, you’ll create your own implementation of Dagger named RayDi. You don’t have to implement an entire app, just the parts of it you need to manage dependency injection in the Server-Repository example. That will be enough to give you a deep understanding of the annotation processor.

@RayDi
class Server() {

  @RayInject
  lateinit var repository: Repository

  fun receive(data: Data) {
    repository.save(data)
  }
}
@RayBind(Repository::class)
class FakeRepository : Repository {

  var collection: MutableList<Data> = mutableListOf()

  override fun save(data: Data) {
    collection.add(data)
    println("Added $data")
  }
}

Diving into the RayDi project

Open the RayDi project in the material for this chapter and you’ll see the code structure shown in Figure 6.3:

Figure 6.3 — The RayDi Project Structure
Yunuza 2.1 — Blu PefMo Yxayikf Vpkumkuju

Annotation

The annotation module contains the definitions of @RayDi, @RayInject and @RayBind. All those annotations are similar — they only differ in their names and targets.

// 1
@Retention(AnnotationRetention.SOURCE)
// 2
@Target(AnnotationTarget.CLASS)
// 3
annotation class RayBind(val interfaceClazz: KClass<*>)
interface RayDiObjectFactory<T> {
  fun create(): T
}

Processor

The processor module contains code to handle annotation processing when building the project.

apply plugin: 'kotlin'
// 1
apply plugin: 'kotlin-kapt'

repositories {
  mavenCentral()
}
// 2
compileKotlin {
  kotlinOptions {
    jvmTarget = "1.8"
  }
}
compileTestKotlin {
  kotlinOptions {
    jvmTarget = "1.8"
  }
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
  // 3
  implementation project(':annotation')
  // 4
  implementation "com.squareup:kotlinpoet:$kotlinpoet_version"
  // 5
  implementation "com.google.auto.service:auto-service:$auto_service_version"
  kapt "com.google.auto.service:auto-service:$auto_service_version"
}
Figure 6.4 — The build task for the app module
Xiveqi 6.1 — Xvu keapb yatj qow pqa iwz papota

// 1
> Task :annotation:compileKotlin
> Task :annotation:compileJava
> Task :annotation:classes
> Task :annotation:jar
// 2
> Task :processor:kaptGenerateStubsKotlin
> Task :processor:kaptKotlin

> Task :processor:compileKotlin
> Task :processor:compileJava
> Task :processor:classes
> Task :processor:jar

// 3
> Task :app:kaptGenerateStubsKotlin
> Task :app:kaptKotlin
> Task :app:compileKotlin
> Task :app:compileJava
> Task :app:classes
> Task :app:jar
> Task :app:assemble
> Task :app:testClasses
> Task :app:build

App

The app module contains the code for the app. In the Server-Repository example, this module contains the Server and Repository definitions you saw earlier. Again, take a closer look at build.gradle’s contents:

apply plugin: 'kotlin'
// 1
apply plugin: 'kotlin-kapt'
// 2
kapt {
  generateStubs = false
}

kotlin {
  // 3
  sourceSets.getByName("main") {
    kotlin.srcDir("${buildDir.absolutePath}/generated/source/kaptKotlin/")
  }
}

dependencies {
  implementation project(path: ':annotation')
  // 4
  kapt project(':processor')
}

Generating code

Build the RayDi app by selecting the build task from the Gradle window and double-clicking the build option. Open build/generated/source/kaptKotlin/main, as shown in Figure 6.5:

Figure 6.5 — The generated code structure
Koqema 0.2 — Mva mihavenuv ragu xmvosxadu

class Server_RayDiFactory : RayDiObjectFactory<Server> {
  override fun create(): Server = Server()
}
class Repository_RayDiFactory : RayDiObjectFactory<Repository> {
  override fun create(): Repository = FakeRepository()
}
class RayDiFactory {
  @Suppress("UNCHECKED_CAST")
  fun <T> get(type: KClass<out Any>): T {
    val target = when (type) {
      Server::class -> Server_RayDiFactory().create()
          .apply {
            repository = Repository_RayDiFactory().create()
          } as T
      Repository::class -> Repository_RayDiFactory().create() as T
      else -> throw IllegalStateException()
    }
    return target as T
  }
}
fun main() {
  val server: Server = RayDiFactory()
      .get(Server::class)
  server.receive(Data("Hello"))
}
Added Data(name=Hello)

Beginning with Dagger

At this point, you not only know how to start working with Dagger, but you also have a deep understanding of the type of code it’s going to generate. You just need to learn what specific annotations it uses and how you can give it the information it can’t infer from the code. It works exactly as you’ve seen in RayDi’s annotation processor.

DaggerServerRepository

Use IntelliJ to open the DaggerServerRepository project from the starter folder in the materials for this chapter. It’s a very simple project with the file structure shown in Figure 6.6:

Figure 6.6 — DaggerServerRepository code structure
Sevoho 1.3 — ZalxezGaybodGijibovixc qixo znmohqeta

class Server() {

  lateinit var repository: FakeRepository // HERE

  fun receive(data: Data) {
    repository.save(data)
  }
}
class FakeRepository : Repository {

  var collection: MutableList<Data> = mutableListOf()

  override fun save(data: Data) {
    collection.add(data)
    println("Added $data")
  }
}
fun main() {
  // Get the reference to a Server instance
  // Invoke the receive() method
}

Installing Dagger

As you learned in the previous paragraphs, Dagger is just a code generator tool implemented as an annotation processor. Therefore, adding Dagger to your project is as simple as adding a few definitions to build.gradle for your project’s module.

Figure 6.7 — build.gradle file for the DaggerServerRepository project
Jojoya 6.9 — kiomc.dlifne kaqe jow fma TiynahJolcutMorugukarc szenisb

plugins {
  id 'org.jetbrains.kotlin.jvm' version '1.4.10'
  // 1
  id "org.jetbrains.kotlin.kapt" version "1.4.10"
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
  mavenCentral()
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib"
  // 2
  implementation "com.google.dagger:dagger:2.28"
  // 3
  kapt "com.google.dagger:dagger-compiler:2.28"
}
Figure 6.8 — “Sync Project with Gradle” file button
Hebowi 1.8 — “Qyqv Lfurugp xizp Ywotre” vaya mumduv

@Component

In the Server-Repository example with RayDi, you generated RayDiFactory as the factory for all the components of the dependency graph. Dagger generates an equivalent class by defining a @Component.

// 1
@Component
// 2
interface AppComponent {
  // 3
  fun server(): Server
}
[Dagger/MissingBinding] com.raywenderlich.android.daggerserverrepository.Server cannot be provided without an @Inject constructor or an @Provides-annotated method.
public abstract interface AppComponent {

@Inject

In the previous paragraph, you learned how to use @Component to tell Dagger what you need. You asked for a Server, but Dagger doesn’t know how to create one. To fix this problem, you’ll use @Inject to tell Dagger where to get the information it needs.

class Server @Inject constructor() { // HERE
  lateinit var repository: FakeRepository

  fun receive(data: Data) {
    repository.save(data)
  }
}

Looking at the generated code

Now, the build was successful and Dagger generated some code. To see it, open build/classes/java/main and look at the structure, shown in Figure 6.9:

Figure 6.9 — Dagger-generated code
Votaza 3.4 — Ceskeb-tetovaruy ziyo

Using the @Component

In the previous paragraph, you learned how to define a @Component — but how do you use it? That’s what you’ll cover next.

fun main() {
  // 1
  val server = DaggerAppComponent
      .create()
      .server()
  // 2  
  server.receive(Data("Hello"))
}
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialized
  at com.raywenderlich.android.daggerserverrepository.Server.receive(Server.kt:42)
class Server @Inject constructor() {

  @Inject // HERE
  lateinit var repository: FakeRepository

  fun receive(data: Data) {
    repository.save(data)
  }
}
class FakeRepository @Inject constructor() : Repository {
  // ...
}
Added Data(name=Hello)

Using @Module and @Provides

In the previous sections, you managed to get a Server that delegates the persistence of some Data to a FakeRepository. In the RayDi example, the Server class was different because the type of the repository variable was Repository.

class Server @Inject constructor() {

  @Inject
  lateinit var repository: Repository // HERE

  fun receive(data: Data) {
    repository.save(data)
  }
}
error: [Dagger/MissingBinding] com.raywenderlich.android.daggerserverrepository.Repository cannot be provided without an @Provides-annotated method.
public abstract interface AppComponent {
// 1
@Module
object MainModule {
  // 2
  @Provides
  fun provideRepository(): Repository = FakeRepository()
}
class FakeRepository : Repository { // HERE
  // ...
}
@Component(modules = arrayOf(MainModule::class)) // HERE
interface AppComponent {

  fun server(): Server
}
Added Data(name=Hello)

Key points

  • Dagger is just a code generation tool, helping you implement the dependency injection pattern in your project.
  • An annotation processor allows you to improve performance when generating code, by moving the source code parsing from runtime to build time.
  • Implementing an annotation processor can be quite complicated, but frameworks like KotlinPoet help in the code generation phase.
  • Installing Dagger follows the same procedure as installing any other annotation processor.
  • A Dagger @Component is the factory for the objects in the dependency graph.
  • You can use @Inject to tell Dagger how to create an instance of a class.
  • JSR-300 defines annotations like @Inject and others that you’ll learn about in the following chapters.
  • @Inject also lets you tell Dagger what property to inject.
  • @Module is a way to provide additional information to help Dagger create instances of classes of the dependency graph.
  • @Provides tells Dagger which function to invoke to provide an instance of a specific type.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now