Conditional Provisioning with Dagger

In this article, you'll learn how to use Dagger for conditionally provisioning (providing) your dependencies without adding stress to your memory.

As your product grows. You often start trying out new things with it. Since we don't always know what'll work best for the users, we often try multiple strategies to solve a problem. This is done through some sort of an A/B test. However, this comes at a cost. The cost is that two distinct solutions exist in the same version of the app, to solve one customer pain point. Let's try to see it through a lens of an image editing application.

Introducing the Sample App

Imagine you have an app that allows users to edit photos and share them. For editing photos, we use a library called SimplePhotoEditor. This library could be your in-house implementation, a third-party proprietary solution, or even an open-source project that you are using, but that is irrelevant and I wanted to get that out of the way.

Now you come across a library called ComplexPhotoEditor which offers you better photo editing capabilities and promises higher performance when compared to its alternatives in the market.

As you are a very dedicated and passionate engineer, you decide that you want to give this new library a try in your app and see if your users find your app to be more useful, robust, and whatnot.

Understanding the Sample Use Case

Both the libraries require a lot of memory as soon as they are initialized and want to hold on to a significant amount of it, throughout their lifetime during the app's session. Not going to comment on the design of the library :]

So as soon as someone initializes an object of either of the two libraries we'd see a significant spike in resource allocation. Since we need at least one of the two libraries at all given times, we'll have to live with that overhead, however, having both of them taking up their fair share of resources doesn't really make sense. So in the next section, we'll look into implementing this behavior.  

Implementing the Use Case

Let's say we have an interface called ImageManipulationStrategy which has a method to manipulate the input and return an output.

interface ImageManipulationStrategy {
    fun manipulate(image: Image): Image
}

In order to support both the libraries, we have their respective implementations in our app, that look like this

class SimpleImageManipulationStrategy(
	private val SimpleImageEditor: simpleImageEditor
    ): ImageManipulationStrategy {
    
    	override fun manipulate(image: Image): Image {
        	// manipulate with simpleImageEditor
    	}

}

and this

class ComplexImageManipulationStrategy(
	private val ComplexImageEditor: complexImageEditor
    ): ImageManipulationStrategy {
    
    	override fun manipulate(image: Image): Image {
        	// manipulate with complexImageEditor
    	}

}

Now when using these strategies in other classes we can easily swap them in our Dagger Module like this

@Provides
fun imageManipulationStrategy(
    simpleImageManipulationStrategy: SimpleImageManipulationStrategy,
    complexImageManipulationStrategy: ComplexImageManipulationStrategy,
) : ImageManipulationStrategy {
    // provide the desired strategy
}

While this works fine, there is a problem with providing the dependency this way and we'll see that in the next section.

Understanding the Problem

As we can see Dagger at Runtime will not know which dependency we actually might get in our method, so it'll initialize both of them. So that whichever one we need, is readily available at our disposal in the method.

Having both of them initialized when we only know one is going to be needed, is the problem here. Now, this can occur in any case, where initializing a class is expensive and you might have to choose between those implementations, wasting resources on at least one of them.

The solution to this problem is pretty simple and we'll see that in the next section.

Understanding the Solution

Dagger exposes the Provider<T> class to us. So instead of using the default mechanism of providing the dependencies, we can defer the initialization by using this class.

The way it works is that Dagger will provide you the Provider<T> object and you can use it to get() the actual dependency.

Here is how it'd look in practice

@Provides
fun imageManipulationStrategy(
    simpleProvider: Provider<SimpleImageManipulationStrategy>,
    complexProvider: Provider<ComplexImageManipulationStrategy>,
) : ImageManipulationStrategy {
    return if (myConditionIsMet) {
        simpleProvider.get()
    } else {
        complexProvider.get()
    }
}

and this will ensure that Dagger doesn't initialize a dependency unnecessarily when we don't need it to.

Conclusion

  • You don't always want to rely on Dagger to provision dependencies for you directly.
  • You should always assess if any of your dependencies are hungry for resources.
  • It is possible to conditionally provision dependencies in your graph at runtime, with Dagger.
  • Dagger exposes a class called Provider
  • When using an object of Provider<T>, Dagger won't initialize the dependency unless the get() method on this object is called.