Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

9. More About Modules
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 previous chapter, you learned some important concepts about Dagger @Modules. You saw how to split the bindings for your app into multiple files using abstract classes, objects, companion objects and interfaces. You learned when and how to use the dagger.Lazy<T> interface to improve performance. And you used the Provider<T> interface to break cycled dependencies.

In this chapter you’ll learn :

  • The benefits of using the @Binds annotation in a @Module.
  • How to provide existing objects. The Android Context is a classical example.
  • When optional bindings can help.
  • How to provide different implementations of the same abstraction using qualifiers with the @Named annotation.
  • When to create custom qualifiers to make the code easier to read and less error-prone.
  • How Android Studio can help you navigate the dependency tree.

As you can see, there’s still a lot to learn about @Modules.

Note: In this chapter, you’ll continue working on the RaySequence app but by the next one, you’ll have all the information you need to migrate the Busso App to Dagger.

More about the @Binds annotation

You’ve already learned how to use @Binds to bind an abstract type to the implementation class Dagger considers fit for that type. But @Binds has other benefits, as well. You’ll see proof of that next.

Open AppModule.kt and replace the existing code with the following:

@Module(includes = [AppBindings::class])
class AppModule {

  @Provides
  fun provideSequenceGenerator(): SequenceGenerator<Int> =
      FibonacciSequenceGenerator()
}

Here, you’re using FibonacciSequenceGenerator because it has a default constructor. You’ll see how to deal with the NaturalSequenceGenerator in a @Binds scenario later in the chapter.

Now,look at the di package in build/generated/source/kapt/debug, as shown in Figure 9.1:

Figure 9.1 — Generated code with @Provides in @Module
Figure 9.1 — Generated code with @Provides in @Module

As you see, there are two different files:

  • AppModule_ProvideSequenceGeneratorFactory.kt, with 29 lines of code.
  • DaggerAppComponent.kt, with 73 lines of code.

You also know that you can provide an instance of the SequenceGenerator<Int> implementation using @Binds. Replace the previous code with the following:

@Module(includes = [AppBindings::class])
interface AppModule {

  @Binds
  fun bindsSequenceGenerator(impl: FibonacciSequenceGenerator):
      SequenceGenerator<Int>
}

Because you’re now delegating the creation of FibonacciSequenceGenerator to Dagger, you also need to change FibonacciSequenceGenerator.kt by adding the @Inject annotation, like so:

class FibonacciSequenceGenerator @Inject constructor() : SequenceGenerator<Int> {
  // ... 
}

Now, you can build again and check what’s in build/generated/source/kapt/debug, as shown in Figure 9.2:

Figure 9.2 — Generated code with @Binds in @Module
Figure 9.2 — Generated code with @Binds in @Module

As you can see, you now have just one file:

  • DaggerAppComponent.kt with 62 lines of code.

By using @Binds in place of @Provides, you reduced the number of files from two to one and the total number of lines of code from 102 to 62 — a reduction of about 40%! This has a great impact on your work: Fewer classes and lines of code mean faster building time.

@Binds was added in Dagger 2.12 specifically to improve performance. So you might wonder, why not always use @Binds? In theory, they’re the best choice, but:

  • In practice, you don’t always have an abstraction for a specific class.
  • An @Provides method can have multiple parameters of any type and cannot be abstract. A @Binds function must be abstract and can only have one parameter that must be a realization of its return type.
  • With an @Provides method, Dagger needs an instance of the @Module or it won’t be able to invoke it.
  • On the other hand, a @Provider can have some logic that decides which implementation to use based on some parameter values.

These are all aspects you need to consider before choosing the best option for you.

Providing existing objects

As you’ve learned, the @Component implementation is the Factory for the objects of the dependency graph. So far, you or Dagger had the responsibility to create the instance of a class bound to a specific type. But what happens if the object you want to provide already exists? A practical example can help.

class SequenceViewBinderImpl @Inject constructor(
    private var sequenceViewListener: SequenceViewBinder.Listener,
    // 1
    private val context: Context
) : SequenceViewBinder {
  // ...
  override fun showNextValue(nextValue: Int) {
  	// 2
    output.text = context.getString(R.string.value_output_format, nextValue)
  }
  // ...
}
@Module
class ContextModule(val context: Context) { // HERE

  @Provides
  fun provideContext(): Context = context
}
@Component(modules = [
  AppModule::class,
  ContextModule::class // HERE
])
@Singleton
interface AppComponent {

  fun inject(mainActivity: MainActivity)
}
Figure 9.3 — The DaggerAppComponent doesn’t compile
Xiwika 6.9 — Tso XuqgigEgkGilwedorf geurr’f lotdita

class MainActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    DaggerAppComponent
        .builder() // 1
        .contextModule(ContextModule(this)) // 2
        .build() // 3
        .inject(this)
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    viewBinder.init(this)
  }
  // ...
}
Figure 9.4 — RaySequence in execution
Tahaba 0.1 — PufQolainzo ur ahofukaij

Using optional bindings

What happens if a binding isn’t present? So far, you get a compilation error, but sometimes you need to make a binding optional.

@Singleton
class SequenceViewBinderImpl @Inject constructor(
    // 1
    private val context: Context
) : SequenceViewBinder {

  // 1
  @Inject
  var sequenceViewListener: SequenceViewBinder.Listener? = null

  override fun init(rootView: MainActivity) {
    output = rootView.findViewById(R.id.sequence_output_textview)
    rootView.findViewById<Button>(R.id.next_value_button).setOnClickListener {
      sequenceViewListener?.onNextValuePressed() // 2
    }
  }
  // ...
}
error: [Dagger/InjectBinding] Dagger does not support injection into private fields
public final class SequenceViewBinderImpl implements SequenceViewBinder {
   @Inject
   @Nullable
   private Listener sequenceViewListener; // HERE
   @Nullable
   public final Listener getSequenceViewListener() {
      return this.sequenceViewListener;
   }

   public final void setSequenceViewListener(@Nullable Listener var1) {
      this.sequenceViewListener = var1;
   }   
  // ...
}
@Singleton
class SequenceViewBinderImpl @Inject constructor(
    private val context: Context
) : SequenceViewBinder {

  @set:Inject // HERE
  var sequenceViewListener: SequenceViewBinder.Listener? = null
  // ...
}
public final class SequenceViewBinderImpl implements SequenceViewBinder {
   @Nullable
   private Listener sequenceViewListener;
   @Nullable
   public final Listener getSequenceViewListener() {
      return this.sequenceViewListener;
   }

   @Inject // HERE
   public final void setSequenceViewListener(@Nullable Listener var1) {
      this.sequenceViewListener = var1;
   }
}
@Module
abstract class AppBindings {
  // ...
  /*
  @Binds
  abstract fun bindViewBinderListener(impl: SequencePresenter):
      SequenceViewBinder.Listener
  */    
}
SequenceViewBinder.Listener cannot be provided without an @Provides-annotated method.

Using @BindsOptionalOf

Open SequenceViewBinderImpl.kt and change the property definition like this:

@Singleton
class SequenceViewBinderImpl @Inject constructor(
    private val context: Context
) : SequenceViewBinder {

  @set:Inject
  var sequenceViewListener: Optional<SequenceViewBinder.Listener> = Optional.absent() // 1

  override fun init(rootView: MainActivity) {
    output = rootView.findViewById(R.id.sequence_output_textview)
    rootView.findViewById<Button>(R.id.next_value_button).setOnClickListener {
      // 2
      if (sequenceViewListener.isPresent) {
        sequenceViewListener.get().onNextValuePressed()
      }
    }
  }
  // ...
}
@Module
abstract class AppBindings {
  @BindsOptionalOf // HERE
  abstract fun provideSequenceViewBinderListener(): SequenceViewBinder.Listener
  // ...
}
@Module
abstract class AppBindings {
  // ...
  @Binds
  abstract fun bindViewBinderListener(impl: SequencePresenter):
      SequenceViewBinder.Listener
}

Using qualifiers

RaySequence contains two different model implementations of SequenceGenerator<T>:

@Module(includes = [AppBindings::class])
interface AppModule {

  @Binds
  fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>

  @Binds
  fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
      SequenceGenerator<Int>
}
error: [Dagger/DuplicateBindings] com...SequenceGenerator<java.lang.Integer> is bound multiple times:

Using the @Named annotation

The easiest way to solve the previous problem is by using the @Named annotation. To do that, just open AppModule.kt and add the following code:

// 1
const val NATURAL = "NaturalSequence"
const val FIBONACCI = "FibonacciSequence"

@Module(includes = [AppBindings::class])
interface AppModule {

  @Binds
  @Named(NATURAL) // 2
  fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>

  @Binds
  @Named(FIBONACCI) // 3
  fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
      SequenceGenerator<Int>
}
@Singleton
class SequencePresenterImpl @Inject constructor(
) : BasePresenter<MainActivity,
    SequenceViewBinder>(),
    SequencePresenter {

  @Inject
  @Named(NATURAL) // HERE
  lateinit var sequenceModel: SequenceGenerator<Int>
  // ...
}
class NaturalSequenceGenerator @Inject constructor( // HERE
    private var start: Int
) : SequenceGenerator<Int> {

  override fun next(): Int = start++
}
error: [Dagger/MissingBinding] java.lang.Integer cannot be provided without an @Inject constructor or an @Provides-annotated method.
const val NATURAL = "NaturalSequence"
const val FIBONACCI = "FibonacciSequence"
const val START_VALUE = "StartValue" // 1

@Module(includes = [AppBindings::class])
interface AppModule {
  // 2
  companion object {
    @Provides
    @JvmStatic
    @Named(START_VALUE) // 3
    fun provideStartValue(): Int = 0
  }
}
class NaturalSequenceGenerator @Inject constructor(
    @Named(START_VALUE) private var start: Int
) : SequenceGenerator<Int> {

  override fun next(): Int = start++
}

Providing values for basic types

Providing a value for common types like Int or String with no qualifier can be dangerous. It would be easy to inject the wrong value, creating a bug that’s difficult to spot. Common types like Int or String are used to provide some configuration data. In that case encapsulation can help.

data class Config(
    val startValue: Int
)
@Module(includes = [AppBindings::class])
interface AppModule {
  // ...
  companion object {
    @Provides
    @JvmStatic
    fun provideConf(): Config = Config(0) // HERE
  }
}
class NaturalSequenceGenerator @Inject constructor(
    config: Config // 1
) : SequenceGenerator<Int> {

  private var start = config.startValue // 2

  override fun next(): Int = start++
}
class NaturalSequenceGeneratorTest {

  @Test
  fun `test natural sequence value`() {
    val naturalSequenceIterator = NaturalSequenceGenerator(Config(0)) // HERE
    listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10).forEach {
      assertEquals(it, naturalSequenceIterator.next())
    }
  }

  @Test
  fun `test natural sequence value starting in diffenet value`() {
    val naturalSequenceIterator = NaturalSequenceGenerator(Config(10)) // HERE
    listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20).forEach {
      assertEquals(it, naturalSequenceIterator.next())
    }
  }
}

Using custom qualifiers

In the previous section, you learned how to bind different definitions to the same type using @Named. To do this, you had to define some String constants to use in your code. Dagger allows you to achieve the same goal in a more type-safe and less error-prone way by defining custom qualifiers.

@Qualifier // 1
@MustBeDocumented // 2
@Retention(AnnotationRetention.BINARY) // 3
annotation class NaturalSequence //4
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
annotation class FibonacciSequence
@Module(includes = [AppBindings::class])
interface AppModule {
  // ...
  @Binds
  @NaturalSequence // 1
  fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>

  @Binds
  @FibonacciSequence // 2
  fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
      SequenceGenerator<Int>
}
@Singleton
class SequencePresenterImpl @Inject constructor(
) : BasePresenter<MainActivity,
    SequenceViewBinder>(),
    SequencePresenter {

  @Inject
  @NaturalSequence // HERE
  lateinit var sequenceModel: SequenceGenerator<Int>
  // ...
}

Modules, bindings & Android Studio

While you were developing the example, you might have noticed some new icons in Android Studio, like the ones you see when you open AppModule.kt:

Figure 9.5 — Android Studio support for Dagger
Yovuqu 8.2 — Adnyaim Nquhuo qogzofy zuq Womkof

Finding a type’s binding

You can find the source of a binding for a specific type by clicking the icon in Figure 9.6:

Figure 9.6 — Find Bindings icon
Qilupu 0.5 — Zuqt Yiqtagfz aday

Figure 9.7 — Source of the binding definition
Jifivi 6.9 — Vaamna ev zba yajhult jivohecual

Finding a type’s usage

In Figure 9.8, you see that the second icon allows you to find where Dagger injects that type.

Figure 9.8 — Find injection for a type
Mibeco 8.6 — Mavs abretzaiz red i wnwo

Figure 9.9 — Modules for a Components
Hiviwa 6.6 — Pucaqek sir u Cuftaqipqv

Key points

  • By using @Binds, Dagger generates fewer and shorter files, making the build more efficient.
  • Dagger @Modules allow you to provide existing objects, but you need to pass an instance of them to the @Component builder that Dagger generates for you.
  • @BindsOptionalOf allows you to have optional bindings.
  • You can provide different implementations for the same type by using @Named.
  • Custom qualifiers allow you to provide different implementations for the same type in a type-safe way.
  • From Android Studio 4.1, you can easily navigate the dependency graph from the editor.
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