Dagger and @Inject on constructors — do or don’t?

Ian Alexander
ProAndroidDev
Published in
6 min readJul 26, 2020

--

Depending who you speak to, putting @Inject on a constructor is either akin to whipping your own grandmother or is such a blindingly obvious thing to do that the question is ridiculous.

But which is right?

The Bobites

These folk have been fully converted to the teachings of Uncle Bob — the father of clean code and clean architecture. Abstraction is our redemption and those who choose to ignore abstraction will burn in the fiery pits of hell.

Or, more simply, Dagger is an implementation detail — a third party library. Our codebase should be able to function regardless of how DI is implemented. Say a library is scattered throughout your code base — this is bad.

Codebases live for a long time, and at some point that library is going to have breaking changes, or become unsupported, or a new shiny library will come out. In an ideal world, changing the library means only a few small changes to a small part of your codebase.

If Dagger implementation details (i.e. @Inject) are scattered throughout your codebase, your codebase becomes resistant to change, not to mention the rest of the codebase has added complexity.

Belief: Never use Dagger goodies like @Inject on constructors!

Principle to live by: Abstraction is king

The Shipits

These folk have seen examples of all those clean architecture code bases and honestly they look more trouble than they’re worth. We’re only trying to build an app, not architect the Golden Gate Bridge.

Dagger has this fancy annotation processing and code generation functionality that makes our lives easier, why on earth would we not use it?

Why would we write a DI definition for every single class in an object graph — duplicating the objects constructor — so any changes to the constructor need made in several places? After all, Dagger will do the heavy lifting for us with one itsy bitsy @Inject annotation.

And let’s be honest, DI frameworks only change once every 2–4 years, what’s the point of abstracting something that more than likely won’t change for the entire time we’ll be working on the project?

Belief: Always use @Inject on constructors!

Principle to live by: Don’t Repeat Yourself (DRY)

When you come down to it, abstraction and DRY code are two fundamental principles of programming — each equally correct. But, in this case, to use one is to break the other. This in itself is nothing new, there’s so many principles of programming that almost any choice you make is bound to break one principle or another.

But if that’s the case, how to choose which principle is more important, so which one you should adhere to? In order to make that choice, you need to evaluate each approach by something.

Many principles of programming have a basis in saving development time. Whether that's sacrificing time now to spend longer implementing a feature which is then incredibly quick to change in the future. Or choosing the quick implementation route now knowing that you’ll pay the debt (and then some) of that saved time on maintenance in the future.

It’s often easier to worry about the present and choose the quick implementation route, even when the time debt you incur is much bigger than the time saved.

Principles we follow — quasi religiously — are to trick our present selves into making better choices which help us write code that takes less time for our future selves, or some other poor developer that takes over our codebase to maintain or change in the future.

So perhaps we can evaluate which principle to choose based on time — the amount of time it takes to implement, maintain, and change.

Let’s see how both methods stack up.

@Inject everywhere

In this hypothetical codebase @Inject is used all over. You can’t look at an object constructor without seeing that pesky annotation. For the developers that wrote it, you can count the seconds to implement with one hand.

Maintenance is also pretty simple. Any time an object in the constructor was changed, Dagger re-generated the object graph definition automatically.

But, one day, a bright spark decided Dagger bad, Koin good. So the team embark on the unenviable task of ridding the codebase of Dagger and adding Koin — coincidently the same team will at some point in the near future be planning on how to migrate to Hilt.

Let’s see how they got on.

A quick find and replace (CMD+SHIFT+R)

And don’t forget, sometimes this part wouldn’t even be needed. After all @Inject is part of Java (JSR330) and is used by other JVM dependency injection libraries like Guice and Toothpick.

All in all this method has been pretty quick. Minimal lines of code needed written in the first place for DI to function, maintenance was a breeze, and removing Dagger from the codebase was super quick (if it even needs removed in the first place).

How does abstraction compare?

Abstracted DI

In this codebase DI has been thoroughly abstracted. DI is fully inside a single package and none of the rest of the codebase has any idea DI exists, let alone what library is used to implement it. So an injection looks something like this:

These are more lines of code as we’re not using Daggers code generation so it has taken longer to write, but we’re sure to save that time in the future.

Now during maintenance when a constructor argument needs added or removed.

Not ideal, every change really requires three changes, and let’s not forget the problem with non-DRY code.

Compile time failure when forgetting to make a change in one of the 3 places.

So maintenance takes significantly longer. But the benefit really comes when switching DI libraries, not a single change needs made in the rest of your codebase. A genuine, if costly (in this case), benefit.

In this slightly contrived example using @Inject is quite clearly quicker. Even in this short example ~1 minute is saved all round by using Dagger’s inbuilt code generation.

This may sound small, but imagine a codebase with multiple developers working for several years, the combined time saving can add up to days of development time. Not to mention the reduction in boilerplate code, after-all, who enjoys writing boilerplate?

But there are drawbacks to using @Inject on constructors and less easily definable benefits to abstraction. For instance:

  • Classes don’t know about DI and DI info is limited to one layer, limiting the complexity overhead of the codebase.
  • Visualising the DI graph is easier with DI code in one package — new tools like Scabbard and the Dagger Visualiser in Android Studio 4.2 make this less important.
  • Implementations are hidden from the object graph so they can’t be accidentally injected.
  • Finding and replacing @Inject across an entire codebase will cause merge headaches if there are multiple active branches when it’s done.

These are very valid concerns. But, do you think these concerns save more time than having DRY code and letting Dagger do the heavy lifting for you?

The answer for most codebases is probably no.

Conclusion

Every choice made while coding is a trade-off. Maybe in your situation it makes complete sense to fully abstract your DI. But before you make that decision, ask yourself, are you making that decision because someone told you to, or are you making that decision because it makes your specific codebase easier (and quicker) to implement, maintain, and change for you and future developers?

Bonus

Even when using @Inject regularly, one common use of manually calling constructors in Dagger modules is for when injecting interfaces rather than implementations. Dagger has a way of dealing with this without duplicating object constructors, @Binds to the rescue.

Vehemently disagree? Stick in a comment!

--

--