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 providesInitializable
- 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>
asBox<? extends Super>
for covariantly definedBox
(orFoo<? super Bar>
for contravariantly definedFoo
) 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.