Deep Dive into Dagger Lazy
A week ago we had a conversation about Dagger Lazy during our weekly dev meeting. I knew it existed but I never felt the need of using it so I thought I could deep dive into Dagger Lazy to better understand it.

Why Dagger Lazy?
When Dagger Lazy was brought up during our dev meeting that’s the first question I asked. Why do we need it? What is the benefit of it? The answer was along the lines of “because is supposed to speed up the start up time of our app” since the initialization of the injected objects is done lazily, meaning that until we actually need those objects they are not going to get initialized saving some precious time.
That sounds pretty good in theory, but I wanted to demonstrate we were actually saving a decent amount of time so I started by writing a simple experiment.
To begin with I’m going to create a new ExpensiveObject
class that, when initialized, fills up an array of ints and bytes. I guess this should be expensive enough :D.
As for the rest of the app we are just simply going to create a new component and module to inject our ExpensiveObject
into the MainActivity
. You can see that we are only using the expensiveObject when the user clicks on a button, that is intentional to demonstrate that Lazy
actually works and it only instantiates the object when is needed.
Testing the start up time without Lazy
First of all we all know that performance testing is tricky and the results we get could vary massively depending on a long list of variables (hot, cold, warm start, etc). To try avoid that I’m going to follow an “advanced” naive approach. I’m going to launch the app 17 times using the same emulator from a cold start. I would then take all the measurements and get the average time. This should give me a bit more of an accurate start up time than If I was just using one measurement.
In order to get the start up time for your app, there are different ways you can use, but the easiest by far is using Displayed
as a filter in the logcat. Below you can see the results with the average value of those times being 1.504ms.

Testing the start up time with Lazy
We are now going to run the same experiment but using Dagger Lazy. All the code will remain the same apart from our MainActivity
which will be using Lazy. You can also see that we need to use .get()
on the injected object to actually get it.
And finally we run our experiment following the same approach as we did before. The result now says that the average start up time of our app is 0.779ms which is indeed quicker than our previous experiment when we didn’t use Lazy.

So what now, do we use Lazy everywhere?
Nope. These results are not enough, before even thinking about using Lazy everywhere we need to measure our real scenarios with our real objects and of course we need to understand the pitfalls of Dagger Lazy.
Even though we have seen it speeds up the start up time, when you actually have objects that are not expensive to create (which is what you would normally have) the difference between both approaches is negligible (a few ms).
You also have to consider the semantics of Lazy. Dagger is injecting a lazy Provider (wrapped in a DoubleCheck) rather the object itself so we now have to use get()
to access the object which is not ideal and adds a bit more code in places where you didn’t need it before.
And finally, you definitely need to understand what Lazy does behind the scenes, so let’s have a look at that. The definition of Lazy in the documentation is as follows:
A handle to a lazily-computed value. Each
Lazy
computes its value on the first call toget()
and remembers that same value for all subsequent calls toget()
.
The way it remembers that value is by memoizing it. If we look at the code generated by Dagger we can see that when we use Lazy, Dagger locks a Provider with a DoubleCheck, whereas without Lazy it just provides the object using the module that we created. You can see that in the code below.
The problem when we try to lazy initialize something comes from the fact that we are in a multi-threading environment and two or more threads could share our lazy initialized field. To avoid that problem we have to use synchronized
, but that comes at a cost.
DoubleCheck is an idiom described in the item 71 of Effective Java 2 and what basically says is that we should avoid the cost of locking when accessing a field that has already been initialized. As the name of the idiom describes, the idea is to check the value twice, first without locking it and if the field is not initialized a second check where we lock the field and initialize it. Without using a double check, the cost of accessing the field would be higher since we would be locking the field every single time we try to access it.
Lazy != Singleton
After reading all that you might be thinking that Lazy is the same as using @Singleton. If we remove Lazy from the MainActivity and add a Singleton annotation to the provides method and the component, the code generated by Dagger looks familiar.
private void initialize(final Builder builder) {
this.provideExpensiveObjectProvider =
DoubleCheck.provider(
MyModule_ProvideExpensiveObjectFactory
.create(builder.myModule));
}
It is using a DoubleCheck to create a Provider for our object but this time is using .provider()
instead of .lazy()
. If we have a look at those methods we can see they are indeed different, provider
caches the value from the given delegate provider whereas lazy
caches the value from the provider.
The documentation has a really good example demonstrating that Lazy != Singleton, you can check that out on this link at the bottom of the page:
https://google.github.io/dagger/api/2.10/dagger/Lazy.html
The last sentence of the documentation sums up everything really well.
Use
@Singleton
to share one instance among all clients, andLazy
for lazy computation in a single client.
What have we learnt?
- When using Lazy, unless you call
get()
, the object is never going to be created. - Subsequent calls to any given instance of
Lazy<T>
will return the same underlying instance ofT
. - Using
Lazy
adds the semantic overhead of having to useget()
. - Lazy locks a Provider with a DoubleCheck making it more costly than not having Lazy.
- Singleton and Lazy are different things, use them accordingly.
- Use Lazy if you have an expensive object that may not get used.
- Always measure and know the internals of what you plan to use. Using Lazy may save you a few milliseconds during start up time but the cost of using DoubleCheck wrapping a Provider and the overhead of having to use
get()
doesn’t make it worth it. - We decided not to use Dagger Lazy.
I hope this article helped you understand what’s going on behind the scenes when you use Dagger Lazy and hopefully you should now be able to make a factual decision about its usage (or not). In the meantime if you have any questions feel free to reach out on Twitter or leave a comment.