Facilitating dependency initialization with Dagger multibindings

Nick Rose
Quick Code
Published in
6 min readFeb 13, 2019

[Note: This article assumes you’ve got a basic knowledge of Dagger2]

As an Android app grows in size so will its number of dependencies. Some dependencies require a one-time initialization when the app starts — analytics and tracking libraries are the usual suspects for this type of dependency. These often end up getting hurled into your custom Application subclass and the result is a big pile of noisy code serving no meaningful purpose to developers other than to distract them and obfuscate potential bugs.

class SlipperySlopeApplication : Application() {

override fun
onCreate() {
super.onCreate()

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}

Foo.initialize(this)

// ...ten thousand other dependency initializations
}
}

Change in software development is an inevitability and being able to mitigate its effects is really important. The class in the example above will have to undergo change every time a new dependency requiring initialization is added to your app. This bloats that class and risks the introduction of bugs. Worse, at some point developers won’t want to even look at the class anymore and will audibly groan when forced to try and understand how to navigate it.

Dagger multibindings provide a neat way to have that class resist change and restore sanity to your app and its developers. This article will go over how to clean up your app initialization code to ultimately yield an Application subclass that looks like:

class LessBadApplication : Application() {

private val appComponent: AppComponent = ...

override fun
onCreate() {
super.onCreate()

appComponent.appInitializer().initialize()
}
}

To get to the state of the code above we’ll wrap every dependency’s initialization algorithm into its own class, signal to Dagger that we’d like to bind those into a collection, then iterate over it and tell each to perform its initialization. This will become clearer in the code examples that follow.

First we’ll start with a simple interface that each dependency initialization wrapper will implement:

interface Initializer {
fun initialize()
}

Now let’s create a class that implements this interface and extracts the Timber initialization code of the first example in this article:

class TimberInitializer @Inject constructor() : Initializer {

override fun initialize() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}

Don’t forget to include an @Inject constructor.

Let’s do the same thing for the made up Foo initialization:

class FooInitializer @Inject constructor(
private val context: Context
) : Initializer {

override fun initialize() {
Foo.initialize(context)
}
}

Note how it’s not a problem if these Initializers have dependencies of their own. If it’s available in your Dagger graph, so can it be in these constructors.

Next we’ll define where the multibinding magic happens. Create a Dagger module like so:

@Module
abstract class InitializerModule {

@Binds
@IntoSet
abstract fun timberInitialize(
timberInitializer: TimberInitializer
): Initializer

@Binds
@IntoSet
abstract fun fooInitializer(
fooInitializer: FooInitializer
): Initializer
}

Using the @IntoSet annotation we are telling Dagger that we want instances of all these new Initializers to be available for use through an injectable Set<Initializer>. Let’s inject such a Set into another custom class:

class AppInitializer @Inject constructor(
private val initializers: Set<@JvmSuppressWildcards Initializer>
) : Initializer {

override fun initialize() {
initializers.forEach(Initializer::initialize)
}
}

This AppInitializer class can then be injected into or exposed to your custom Application subclass and have its initialize() method invoked — one and done! Note that in Kotlin the @JvmSuppressWildcards annotation in this example is necessary for compilation to succeed. You can read more about why here.

Now whenever a new dependency requiring initialization is added, simply encapsulate the initialization algorithm into an Initializer concretion and add it to the list ofInitializerModule bindings. Removing one is just as easy, too. Your class that owns the AppInitializer instance will never again have to change for those reasons and all the app’s dependency initialization will be neatly contained within their own respective files.

You can check out a full working example of this concept here: https://github.com/nihk/DaggerMultibindingsFun

2021/04/01 — update

A response to this article brought up a good point about the order of the set of Initializers not being guaranteed. If one of your Initializers depended on another Initializer being initialized before its own initialization (still following along?), then your app might not behave the way you’d expect it to. For example, if FooInitializer logged to Timber during its initialization and was initialized before TimberInitializer, that log would be lost.

The order of the @IntoSet members cannot be controlled using Dagger’s APIs. I’m not one to throw out the baby with the bathwater, however; I think there is a realistic way to address this while still using Dagger multibindings.

Let’s first consider how we’d have to set up our code if we didn’t use this multibindings pattern, because we wanted to define an initialization order. We’d be back in the bloated SlipperySlopeApplication state I described earlier in this article, but now with order management sprinkled in. That order is obfuscated by the bloat, and it wouldn’t be clear which initialization calls were ordered intentionally and which ones had an order that didn’t actually matter. Take the following, for example:

class SlipperierSlopeApplication : Application() {

override fun
onCreate() {
super.onCreate()

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}

Foo.initialize(this)
Bar.initialize(this)
Baz.initialize()
}
}

If Foo's initialization logged something to Timber, then the Timber planting would need to be called before Foo's call. But what about the Bar and Baz initialization: are they order dependent? It’s not clear.

How can we solve this if we stuck to our guns with Dagger multibindings? One commenter on the response article suggested adding an abstract sortOrder(): Int function on the Initializer interface to help dictate order. I’m not a fan of this idea because an Initializer should not individually care or know about the state of its dependencies or sibling Initializers, and not every Initializer cares what order it should be initialized in, anyway. Having to manage sortOrder values scattered across a large project is also not tenable.

The solution I’d propose to this problem is to simply inject a way to tell AppInitializer (referenced earlier in this article) how to sort its set of Initializers. Once that set is sorted, AppInitializer can iterate through its collection and call initialize on each in a way that’s safe and intentional. A custom Comparator can be the thing that sorts the set:

class AppInitializerComparator(
private val priorities: List<Class<out Initializer>>
) : Comparator<Initializer> {
// ...
}

And used like so:

class AppInitializer @Inject constructor(
private val initializers: Set<@JvmSuppressWildcards Initializer>,
private val comparator: AppInitializerComparator
) : Initializer {

override fun initialize() {
initializers
.sortedWith(comparator)
.forEach(Initializer::initialize)
}
}

The priorities value passed into AppInitializerComparator is what defines the order of Initializer initialization. Only Initializers interested in having an order need to be a part of that collection. For the example I have been talking about so far, priorities would only have one element: a reference to TimberInitializer. A Dagger module is a great place to define what goes into that priorities value, because a Dagger module is a thing that is responsible for configuration. For this example, it’d look like so:

@Provides
fun appInitializerComparator(): AppInitializerComparator {
val priorities = listOf<Class<out Initializer>>(
TimberInitializer::class.java
)
return AppInitializerComparator(priorities)
}

Note the lack of reference to Foo or Bar or Baz initialization. All this priorities value is saying is “Initialize Timber first.” We don’t have to care about the other Initializers. Now, TimberInitializer is guaranteed to be called first by AppInitializer. Foo can later safely be initialized and have a successful log to Timber.

The Comparator implementation of AppInitializerComparator can be done in multiple ways, but the gist of it is that you’ll want to compare the indices of each element, like so:

class AppInitializersComparator(
private val priorities: List<Class<out Initializer>>
) : Comparator<Initializer> {
override fun compare(first: Initializer, second: Initializer): Int {
val firstIndex = priorities.indexOf(first::class.java).handleNotFound()
val secondIndex = priorities.indexOf(second::class.java).handleNotFound()
return firstIndex.compareTo(secondIndex)
}

private fun Int.handleNotFound(): Int {
return if (this == -1) {
Int.MAX_VALUE
} else {
this
}
}
}

Got more than just TimberInitializer that needs an order set? Then simply arrange the priorities collection accordingly. This process is no more manual than having to set their order in your Application.onCreate without Dagger, but it is much cleaner and easier to manage.

--

--