Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

13. Multibinding
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 sections, you learned how Dagger works by migrating the Busso app from a homemade injection framework based on the ServiceLocator pattern to a fully scoped Dagger app. Great job!

Now, it’s time to use what you’ve learned to add a new feature to the Busso App, which will display messages from different endpoints at the top of the BusStop screen. For instance, you can display information about the weather and any traffic problems at your destination.

You also want to let the user add or remove new endpoints in a simple and declarative way. To do this, you’ll implement a small framework called an information plugin framework. Its high-level architecture looks like this:

Figure 13.1 — Information from a server
Figure 13.1 — Information from a server

Busso connects to different endpoints, gets some information and displays the messages at the top of the BusStop screen. This is a very simple use case that allows you to learn what Dagger multibinding is.

In this chapter’s starter project, you’ll find a version of Busso that already contains the first implementation of the information plugin framework.

In this chapter, you’ll examine the existing code, learning:

  • What multibinding is.
  • How to use multibinding with Set.

Multibinding is a very interesting Dagger feature because it simplifies how you integrate new features by using a plugin pattern, which you’ll learn all about in this chapter.

The information plugin framework

As you read in the introduction, Busso already contains a small framework that lets you fetch information to display at the top of the BusStop Fragment.

You can already see this very simple feature at work. Right now, it displays the coordinates of your current location:

Figure 13.2 — The WhereAmI information
Figure 13.2 — The WhereAmI information

Note: If the architecture of the information plugin framework is already clear to you, just skip to the Introducing Dagger Multibinding section ahead.

When you open the Busso project with Android Studio, you’ll see the source directory structure in Figure 13.3:

Figure 13.3 — The initial source directory structure
Figure 13.3 — The initial source directory structure

In particular, you’ll see a new plugins package. Here are its sub-packages and what they contain:

  • api: The main abstraction of the framework.
  • di: Dagger definitions.
  • impl: Implementation of the main abstraction.
  • model: A very simple class that models the information you’ll receive from the server.
  • ui: The presenter and viewbinder of the framework.
  • whereami: The classes you’ll need to implement the feature that displays the coordinates of your current location, as shown in Figure 13.3. This is the first feature that uses the information plugin framework.

An in-depth description of all the code would take too much space and time, so in this chapter, you’ll focus on the aspects related to dependency injection and, of course, Dagger.

Dagger configuration

The main aspect of this framework is the Dagger configuration you find in the plugins.di package. Open InformationPluginModule.kt in plugins.di and look at its code:

interface InformationPluginModule { // 1

  @Module
  interface ApplicationBindings { // 1
    @Binds
    fun bindInformationPluginRegistry(
        impl: InformationPluginRegistryImpl // 2
    ): InformationPluginRegistry
  }

  @Module
  interface FragmentBindings { // 1
    @Binds
    fun bindInformationPluginPresenter(
        impl: InformationPluginPresenterImpl // 3
    ): InformationPluginPresenter

    @Binds
    fun bindInformationPluginViewBinder(
        impl: InformationPluginViewBinderImpl // 3
    ): InformationPluginViewBinder
  }
}

InformationPluginRegistry

When it comes to working with plugins, frameworks, including the information plugin framework, have some important characteristics in common. They all need to:

interface InformationPluginRegistry {

  fun register(spec: InformationPluginSpec) // 1

  fun plugins(): List<InformationPluginSpec> // 2
}
interface InformationPluginSpec {

  val informationEndpoint: InformationEndpoint // 1

  val serviceName: String // 2
}
@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor() : InformationPluginRegistry {

  private val plugins = mutableListOf<InformationPluginSpec>()

  override fun register(spec: InformationPluginSpec) {
    plugins.add(spec)
  }

  override fun plugins(): List<InformationPluginSpec> = plugins
}

The WhereAmI information plugin

To implement an information plugin, you need to follow these steps:

Creating an endpoint

Assuming that you have an actual endpoint to call, your first step is to define an implementation of InformationEndpoint, which you find in plugins.api.

interface InformationEndpoint {

  fun fetchInformation(location: GeoLocation): Single<InfoMessage>
}
interface WhereAmIEndpoint : InformationEndpoint
class WhereAmIEndpointImpl @Inject constructor(
    private val myLocationEndpoint: MyLocationEndpoint // 1
) : WhereAmIEndpoint {
  override fun fetchInformation(location: GeoLocation): Single<InfoMessage> =
      myLocationEndpoint.whereAmIInformation(location.latitude, location.longitude) // 2
}
interface MyLocationEndpoint {
  @GET("${BUSSO_SERVER_BASE_URL}myLocation/{lat}/{lng}")
  fun whereAmIInformation(
      @Path("lat") latitude: Double,
      @Path("lng") longitude: Double
  ): Single<InfoMessage>
}
Figure 13.4 — The InformationEndpoint diagram
Qapami 13.5 — Dki AbgiybotiijIhdyuuhn huokkud

Defining a @Module and creating InformationPluginSpec

Now, you need to tell Dagger how to create the objects it needs for WhereAmI. Open WhereAmIModule.kt in plugins.whereami.di and look at the code:

@Module(includes = [WhereAmIModule.Bindings::class])
object WhereAmIModule {

  @Provides
  @ApplicationScope // 1
  fun provideMyLocationEndpoint(retrofit: Retrofit): MyLocationEndpoint {
    return retrofit.create(MyLocationEndpoint::class.java)
  }

  @Provides
  @ApplicationScope // 2
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
    override val serviceName: String
      get() = "WhereAmI"
  }

  @Module
  interface Bindings { // 3

    @Binds
    fun bindWhereAmIEndpoint(
        impl: WhereAmIEndpointImpl
    ): WhereAmIEndpoint
  }
}
@Component(modules = [
  ApplicationModule::class,
  InformationPluginModule.ApplicationBindings::class,
  WhereAmIModule::class // HERE
])
@ApplicationScope
interface ApplicationComponent {
  // ...
}

Registering InformationPluginSpec

This is the last and most important step in the process. Once you define InformationPluginSpec, you need to register it to InformationPluginRegistry to make it available to the framework. At the moment, you do this in Main.kt, which looks like this:

class Main : Application() {

  lateinit var appComponent: ApplicationComponent

  @Inject
  lateinit var informationPluginRegistry: InformationPluginRegistry // 1

  @Inject
  lateinit var whereAmISpec: InformationPluginSpec // 2

  override fun onCreate() {
    super.onCreate()
    // 3
    appComponent = DaggerApplicationComponent
        .factory()
        .create(this).apply {
          inject(this@Main) // 3
        }
    informationPluginRegistry.register(whereAmISpec) // 4
  }
}
Figure 13.5 — The WhereAmI information
Jumobu 69.7 — Mmo WyafeAhO uyjoqzocoaz

Introducing Dagger multibinding

To understand how multibinding helps implement the information plugin framework, take a moment to go over what you’ve done so far. You basically defined:

Using multibinding with Set

Your first step is to use Dagger multibinding with Set to implement the information plugin registration mechanism. This allows you to:

Refactoring InformationPluginRegistry

You want all the InformationPluginSpecs you register to be in a Set<LocationPluginInfo>, so you need to change InformationPluginRegistry accordingly. To do this, open InformationPluginRegistryImpl.kt in plugins.impl and apply the following changes:

@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val informationPlugins: Set<InformationPluginSpec> // 1
) : InformationPluginRegistry {

  // 2
  override fun plugins(): List<InformationPluginSpec> = informationPlugins.toList() // 3
}
interface InformationPluginRegistry {
  
  fun plugins(): List<InformationPluginSpec>
}

Registering WhereIAm

This is the most interesting part: How can you register your plugin declaratively? Open WhereAmIModule.kt in plugins.whereami.di and you’ll see that you’re already telling Dagger how get an InformationPluginSpec for the WhereAmI plugin:

@Module(includes = [WhereAmIModule.Bindings::class])
object WhereAmIModule {
  // ...
  @Provides
  @ApplicationScope
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
    override val serviceName: String
      get() = "WhereAmI"

  }
  // ...
}
@Module(includes = [WhereAmIModule.Bindings::class])
object WhereAmIModule {
  // ...
  @Provides // 1
  @ApplicationScope // 2
  @IntoSet // 3 HERE
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec { // 4
    override val informationEndpoint: InformationEndpoint 
      get() = endpoint
    override val serviceName: String
      get() = "WhereAmI"

  }
  // ...
}

Cleaning up your code

Open Main.kt and remove the excess code so it looks like this:

class Main : Application() {

  lateinit var appComponent: ApplicationComponent

  override fun onCreate() {
    super.onCreate()
    appComponent = DaggerApplicationComponent
        .factory()
        .create(this)
  }
}

val Context.appComp: ApplicationComponent
  get() = (applicationContext as Main).appComponent

Using @JvmSuppressWildcards

When you build now, you get the following error:

ApplicationComponent.java:8: error: [Dagger/MissingBinding]
java.util.Set<? extends com.raywenderlich.android.busso.plugins.api
.InformationPluginSpec> cannot be provided without an @Provides-annotated method.
  @Provides
  @ApplicationScope
  @IntoSet
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec { 
    override val informationEndpoint: InformationEndpoint 
      get() = endpoint
    override val serviceName: String
      get() = "WhereAmI"

  }

Implementing @JvmSuppressWildcard

To implement this, open InformationPluginRegistryImpl.kt in plugins.impl and apply this change:

@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val informationPlugins: @JvmSuppressWildcards Set<InformationPluginSpec> // HERE
) : InformationPluginRegistry {

  override fun plugins(): List<InformationPluginSpec> = informationPlugins.toList()
}
@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val informationPlugins: Set<@JvmSuppressWildcards InformationPluginSpec> // HERE
) : InformationPluginRegistry {

  override fun plugins(): List<InformationPluginSpec> = informationPlugins.toList()
}
Figure 13.6 — The WhereIAm location plugin feature
Gemehi 23.5 — Fme GfenuUUv tobabuic zqopel deutuqi

Adding a new information service plugin

As you learned in the first chapter of this book, it’s not important how fast you implement a feature but how fast you can change or extend it. To appreciate Dagger’s multibinding capabilities, you’re now going to add a new information plugin.

Defining an endpoint

Create a new package, plugins.weather.endpoint, then add a new file named WeatherEndpoint.kt to it with the following code:

interface WeatherEndpoint {

  @GET("${BUSSO_SERVER_BASE_URL}weather/{lat}/{lng}")
  fun fetchWeatherCondition(
      @Path("lat") latitude: Double,
      @Path("lng") longitude: Double
  ): Single<InfoMessage>
}
interface WeatherInformationEndpoint : InformationEndpoint
class WeatherInformationEndpointImpl @Inject constructor(
    private val weatherEndpoint: WeatherEndpoint
) : WeatherInformationEndpoint {
  override fun fetchInformation(location: GeoLocation): Single<InfoMessage> =
      weatherEndpoint.fetchWeatherCondition(location.latitude, location.longitude)
}

Creating the weather information @Module

Now, you need to create a @Module to tell Dagger about the InformationEndpoint you just created. Create a new package, plugins.weather.di, add a new file named WeatherModule.kt, then give it this code:

@Module(includes = [WeatherModule.Bindings::class])
object WeatherModule {

  @Provides
  @ApplicationScope
  fun provideWeatherEndpoint(retrofit: Retrofit): WeatherEndpoint {
    return retrofit.create(WeatherEndpoint::class.java)
  }

  @Provides
  @IntoSet // HERE
  @ApplicationScope
  fun provideWeatherSpec(endpoint: WeatherInformationEndpoint): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
    override val serviceName: String
      get() = "Weather"

  }

  @Module
  interface Bindings {

    @Binds
    fun bindWeatherInformationEndpoint(
        impl: WeatherInformationEndpointImpl
    ): WeatherInformationEndpoint
  }
}

Adding a weather plugin

To add the WeatherModule to the modules in ApplicationComponent, open ApplicationComponent.kt in di and apply the following change:

@Component(modules = [
  ApplicationModule::class,
  InformationPluginModule.ApplicationBindings::class,
  WhereAmIModule::class,
  WeatherModule::class // HERE
])
@ApplicationScope
interface ApplicationComponent {
  // ...
}

Checking your work

Now, build and run your app and you’ll see something like Figure 13.7:

Figure 13.7 — The Weather location plugin feature
Xowusi 68.2 — Vde Pearpuv peqixeob jhitid laijusa

Figure 13.8 — The Weather location plugin feature
Bijofe 92.8 — Mco Hearzir pakuheuy vmulip xeocaho

More about Multibinding with Set: @ElementsIntoSet

In the previous example, you learned how to use @IntoSet to add a specific binding to a Set that Dagger creates for you and makes available to the dependency graph of the app.

@Module(
    includes = [
      WhereAmIModule::class,
      WeatherModule::class
    ]
)
object InformationSpecsModule {

  @Provides
  @ElementsIntoSet // 1
  @ApplicationScope
  fun provideWeatherSpec(
      @Named(WHEREAMI_INFO_NAME) whereAmISpec: InformationPluginSpec, // 2
      @Named(WEATHER_INFO_NAME) weatherSpec: InformationPluginSpec // 2
  ): Set<InformationPluginSpec> { // 3
    return mutableSetOf<InformationPluginSpec>().apply {
      add(whereAmISpec)
      add(weatherSpec) 
    }
  }
}
const val WHEREAMI_INFO_NAME = "WhereAmI" // 1

@Module(includes = [WhereAmIModule.Bindings::class])
object WhereAmIModule {
  // ...
  @Provides
  @ApplicationScope
  // 2
  @Named(WHEREAMI_INFO_NAME) // 3
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
    override val serviceName: String
      get() = WHEREAMI_INFO_NAME 
  }
}
const val WEATHER_INFO_NAME = "Weather"

@Module(includes = [WeatherModule.Bindings::class])
object WeatherModule {
  // ...
  @Provides
  @Named(WEATHER_INFO_NAME)
  @ApplicationScope
  fun provideWeatherSpec(endpoint: WeatherInformationEndpoint):
      InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
    override val serviceName: String
      get() = WEATHER_INFO_NAME
  }
}
@Component(modules = [
  ApplicationModule::class,
  InformationPluginModule.ApplicationBindings::class,
  InformationSpecsModule::class
])
@ApplicationScope
interface ApplicationComponent {

  fun activityComponentBuilder(): ActivityComponent.Builder

  @Component.Factory
  interface Builder {

    fun create(@BindsInstance application: Application): ApplicationComponent
  }
}
Figure 13.9 — The information plugins in the Busso app
Vumuco 11.9 — Xge ocveblejuec dwokigg af qni Yenzi ant

Key points

  • Dagger multibinding allows you to add functionality to your app in an easy and declarative way.
  • You can use multibinding with both Set and Map.
  • @IntoSet allows you to populate a Set when you initialize the dependency graph.
  • Types are fundamental to Dagger. Use @JvmSuppressWildcards to fix a variance problem that occurs when Dagger resolves the objects to inject.
  • @ElementsIntoSet allows you to put more than one object into a multibinding Set.
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