Facilitating dependency initialization with Dagger multibindings
[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 Initializer
s 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 Initializer
s 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 Initializer
s not being guaranteed. If one of your Initializer
s 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 Initializer
s, 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 Initializer
s. 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 Initializer
s 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 Initializer
s. 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.