Technology

🗡️ Dagger vs. 🧃 Guice

Diwaker Gupta
· 3 min read
Send by email

If you are familiar with dependency injection and you are in Java-land, you’ve probably heard of Google Guice. You may even have heard of this new D-I framework from the fine folks at Square, called Dagger. We have a sizeable codebase that uses Guice heavily, but I’m a sucker for shiny new things so I started playing with Dagger to see what it would take to migrate away from Guice. Read on to learn more.

Why Dagger?

  • Works on Android!
  • Supposedly faster than Guice, because Dagger works by generating code up-front, whereas Guice uses Reflection at runtime.
  • Optional maven compiler plugin can flag various errors at compile time, which can be extremely useful. Examples include:
  • Detect cyclic dependencies
  • Detect unused and duplicate bindings
  • Detect incomplete modules (that don’t provide all bindings required to construct the object graph)
  • Simple (but limited) API compared to Guice
  • Uses standard JSR annotations where possible

All this sounds good, so what are were some of the problems I ran into?

Pain Points

Dagger has several restrictions on what can be injected and where. In most cases these restrictions are good in that they typically point to code that needs restructuring/refactoring. However, in several cases I found Dagger to be overly restrictive and found myself jumping through hoops just to get things to work.

No cyclic dependencies

Cyclic dependencies are generally frowned upon and should be avoided. That said, in large code-bases, they are a practical reality and need to be addressed. If you can’t break the dependency, use Lazy<T> to work around it.

Injectable objects MUST have @Inject constructor

Dagger will not inject objects that are not annotated with @Inject. Unlike Dagger, Guice can auto-inject objects with a no-arg constructor, which is extremely handy, especially when dealing with 3rd party libraries. This restriction is OK for your own code, since it forces developers to think about injection and makes the intent explicit, thus avoiding bugs due to accidental/unintended injections. But when working with 3rd party libraries and Dagger, you’ll end up with lots of boilerplate Providers like:

@Provides
ThirdPartyFoo provideThirdPartyFoo() {  
  return new ThirdPartyFoo();
}

No shortcut to bind interfaces to implementations

Guice has several handy bind methods that make it easy to bind interfaces to implementations. This is particularly useful when using different bindings in production vs. testing. Unfortunately Dagger doesn’t have any such facility, resulting in some more boilerplate Providers like:

@Provides
@Singleton
Foo provideFoo(FooImpl fooImpl)  
  return fooImpl;
}

Restrictions, restrictions

Dagger imposes several restrictions on modules, provider methods, injectable objects that Guice does not. If you’ve been using Guice, you’ll likely run into one or more of these:

  • Modules can’t be abstract or private
  • @Provides methods can’t be static
  • Can’t inject private or final members
  • Can’t inject constructors that throw exceptions: see #266

Injecting unit tests

We use junit4 for tests and most of our tests require injection for some fields. With Guice, one can simply have a TestRule that injects the members:

// module is a test module that overrides production settings
Guice.createInjector(module).injectMembers(target);

Unfortunately, Dagger requires modules to declare entry points via the injects clause and there doesn’t seem any way to relax that. This effectively makes it impossible to use a Rule based approach like above. Instead, each test has to specify it’s own module:

class MyTest {  
  @Module(injects = MyTest.class, includes = TestModule.class)
  static class MyTestModule {
  }
}

This results in a lot of boilerplate code across all tests. I understand the ‘injects’ clause allows Dagger to flag errors up front, but there should be an easier way to inject members in unit test. Right now, this is effectively a blocker for me. Tracked by #269

Other pitfalls

  • While Dagger is being used in production, it is still a young piece of software. Be prepared for weird bugs in the short-term (e.g. #258)
  • Leave no Guice imports behind!: Dagger depends on JSR javax.injectannotations. When migrating from Guice, make sure to eliminate ALL com.google.inject annotations, or weird things will happen.
  • Use @Singleton consistently: say you have an interface Foo and an implementation FooImpl. As noted above, you’ll need a provider method to inject Foo with FooImpl. If you annotate this method with @Singleton, you should annotate the implementation with @Singleton as well. Otherwise, if any call site injects the implementation directly, bad things will happen.

Conclusion

The biggest benefit of Dagger for me is the compiler plugin — the value-add of detecting dependency problems at compile time is huge. I also find the limited API surface appealing — there are too many ways of doing one thing in Guice. I did get the application working with Dagger but performance benefits, if any, were negligible.

Unfortunately, there are still gaps in Dagger that might make it difficult for some code bases to migrate. The only true blocker for me right now is lack of support for injecting unit tests. Definitely look forward to using Dagger in my projects!