IntoSet: Dagger Multibindings and Architecture

Featured in Kotlin Weekly #240

Dagger is typically my DI framework of choice for large projects (insert controversy here) and while most people are familiar with the basic concepts, some aspects immediately cause internal panic when mentioned. Multibindings is one of them, which is a shame because it’s a highly useful pattern which can solve real problems.

There’s two different types - Set and Map multibinding, and for today I’ll be showing some handy usecases for Sets that we’ve found at Cuvva.

Intro to IntoSet

In a nutshell, Set multibinding allows you to inject a collection of a specific type into other classes, and also provides a way for you to inject a specific class into that collection.

For a truly contrived example, here we provide 4 different String objects and supply them in a Set:

@Module
class ExampleModule {

    @Provides
    @IntoSet
    fun provideA(): String = "A"

    @Provides
    @IntoSet
    fun provideB(): String = "B"

    @Provides
    @ElementsIntoSet
    fun provideMultipleObjects(): Set<String> = setOf("C", "D")
}

Now we’re able to pick up this Set inside other classes like so and consume them:

class SimpleClass @Inject constructor(
    private val strings: Set<@JvmSuppressWildcards String>
) {

    init {
        check(strings.containsAll("A", "B", "C", "D"))
    }
}

It’s a neat party trick and it’s simple enough to set up, but what is this pattern actually useful for?

Registering Components

Set Multibindings are a fantastically useful tool in multi-module projects where you might want a Gradle module to register some specific type without providing it a dependency on some form of manager for that type, or conversely having that manager class aware of all of the classes it’s responsible for. It allows you to very effectively decouple the implementation from where the type is consumed.

An obvious example of this is classes that require initialization using the Application Context - typically these are third-party SDKs which you should be keeping as isolated as possible using facades. For this, we created an interface imaginatively called Initializable:

interface Initializable {

    fun init(application: Application)
}

Any class that requires init at runtime can be wrapped in that interface and be set up - Timber being a good example:

internal class TimberInitializer @Inject constructor() : Initializable {

    override fun init(application: Application) {
        if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
    }
}

We then bind this Initializable into the Set using a @Module:

@Module
abstract class LoggerModule {

    @Binds
    @IntoSet
    abstract fun bindTimber(timber: TimberInitializer): Initializable
}

This Set is then picked up in a class called AppInitializers which lives in the :app Gradle module, where we iterate over the collection and call each Initializable’s init function:

class AppInitializers @Inject constructor(
    private val initializers: Set<@JvmSuppressWildcards Initializable>
) {

    fun init(application: Application) {
        initializers.forEach { it.init(application) }
    }
}

Finally, we actually call init in the Application itself:

class CuvvaApplication : DaggerApplication() {

    @Inject
    lateinit var initializers: AppInitializers

    override fun onCreate() {
        super.onCreate()

        initializers.init(this)
    }
}

The great thing about this is that the Application isn’t at all aware that Timber exists. We can now use Timber, wrapped in a facade that we’ve defined, anywhere in our app without polluting our codebase with a third-party dependency. If we choose to migrate to some other logger, we do it in one module only - this keeps the impact of changing APIs very small.

Moshi Adapters

Another example of where we use this pattern is in our data layers, specifically for registering JsonAdapter classes for Moshi. Sometimes you’ll be dealing with a specific type returned from your backend API which Moshi doesn’t know how to deal with, so you have to create a class to decode/encode it:

class SomeTypeAdapter @Inject constructor() : JsonAdapter<SomeType>() {

    @FromJson
    override fun fromJson(reader: JsonReader): SomeType? {
        // Impl
    }

    @ToJson
    override fun toJson(writer: JsonWriter, value: SomeType?) {
        // Impl
    }
}

These classes get registered with Moshi like so:

Moshi.Builder()
    .add(SomeTypeAdapter())
    .build()

However, you likely don’t want to make your central Moshi instance public and pass it to all data modules, nor do you want to have every data module depend on a central networking module - we aim to develop features in an independent vertical slice without too many other module dependencies (with varying degrees of success). @IntoSet again gives us a nice way of registering these adapter classes and picking them up in our networking module, with that module unaware of any others:

@Provides
@Singleton
internal fun provideMoshi(
    adapters: Set<@JvmSuppressWildcards JsonAdapter<*>>,
): Moshi = Moshi.Builder()
    .apply { adapters.forEach { add(it) } }
    .build()

A real world example

This particular setup really proved its worth recently when we migrated our app’s QA reporting tool from Instabug to ShakeBugs. ShakeBugs, like many SDKs has to be initialized, but it also handily provides an Interceptor class which allows us to see exactly what was happening on the network when a colleague reports a bug. Consequently, the DI module for :analytics:shakebugs looks like so:

@Module(includes = [ShakeBugsModule.BindsModule::class])
class ShakeBugsModule {

    @Provides
    @IntoSet
    internal fun provideInterceptor(): Interceptor = ShakeNetworkInterceptor()

    @Module
    internal interface BindsModule {

        @Binds
        @IntoSet
        fun bindInitializer(initializer: ShakeBugsInitializer): Initializable
    }
}

This public class ShakeBugsModule is the only public class in the module, as every other implementation detail is hidden behind an interface. What’s more, this Gradle module only relies on 4 things:

  • The ShakeBugs SDK
  • :initializable, a Gradle module that provides Initializable
  • A Gradle module that provides feature flagging
  • Dagger

I’m really pleased with how this pattern turned out, and there’s quite a few other Gradle modules set up this way.

JvmSuppressWildcards

You might have noticed the ugly @JvmSuppressWildcards annotation. This is an unfortunately quirk of Kotlin’s interop with Java. According to the reference:

To make Kotlin APIs work in Java we generate Box<Super> as Box<? extends Super> for covariantly defined Box (or Foo<? super Bar> for contravariantly defined Foo) when it appears as a parameter.

This sounds absolutely terrifying on first glance, but the long and short of it is this:

  • In the first contrived example, we’re injecting Set<String>
  • Under the hood, Kotlin generates Java code from our definition
  • The definition in Java would be Set<? extends String> where ? is a wildcard
  • Dagger doesn’t quite know how to handle this, so we add @JvmSuppressWildcards
  • This changes the definition in Java back to Set<String>

It’s not as bad as it sounds. I highly recommend you read through the reference on generics in Kotlin, which has a pretty clear explanation for those interested. For those that aren’t - don’t forget @JvmSuppressWildcards, or your app won’t compile!

Ordering and interactions

This pattern has worked really well for us so far. However, there’s a drawback worth keeping in mind.

A downside of using Set Multibindings is there’s absolutely no guarantee that the collection you’re injecting will be in any particular order. This can be quite tricky and has been the source of a couple of really great, subtle bugs for us - for instance we have a network interceptor that parses errors from our backend, but this must be first in the chain, otherwise another interceptor could break. These interactions can be difficult to debug and reason about because they’re fundamentally non-deterministic.

You’ll also likely find this situation at somepoint when using Initializable, where one class that requires setup has to be downstream of another.

Google has a solution to this particular problem in the form of the App Startup library, which lets you specify dependencies. There’s also other solutions - for instance, Initializable could also have an overridable priority property which the Set is then sorted by.

However you solve the problem it’s a very real one that can cause some head scratching, so bear that in mind before you convert lots of classes to use this patten - it can definitely be over-used and the more you provide this way, the more likely it is that you’ll come across one of these scenarios. This is where you’ll want to start thinking more about integration or even end-to-end testing to verify that classes provided from one module don’t interact negatively with another.

Thank you

Thank you for reading this far. If these sorts of architecture problems interest you, we’re hiring.

comments powered by Disqus