Skip to content

Dagger 2.17 @Binds bugs

Ron Shapiro edited this page May 31, 2018 · 2 revisions

Dagger 2.16 and below have a bug where bindings from @Binds methods are not properly resolved in the components that install their enclosing Module, and instead are resolved at the component that first uses the binding. This could result in confusing behavior, especially around scoping.

What does this practically mean?

If a @Binds method is installed in a component, but has a transitive dependency on a binding in a child component, the binding will silently "float down" to the child component. This is not the same behavior as an equivalent @Provides method, and will soon be fixed.

The bug is fixed in Dagger 2.17, and many components will continue to build when upgraded to 2.17, but some may break because that transitive dependency cannot be satisfied in the parent component or because the binding's scope matches the child instead of the parent component. This page has information on the bug and how to fix your code should you encounter a compilation failure when upgrading.

Example #1:

@Singleton
@Component(modules = FooModule.class)
interface ParentComponent {
  ChildComponent child();
}

@Module
abstract class FooModule {
  @Binds
  @ChildScope
  abstract Foo bind(FooImpl fooImpl);
}

@ChildScope
@Subcomponent
interface ChildComponent {
  Foo foo();
}

We'd expect that this configuration should fail because Foo is installed in ParentComponent but ParentComponent does not have @ChildScope. However, Dagger currently does not detect this correctly and allows Foo's binding to be resolved in ChildComponent because that's the first usage of the Foo key, and so the component compiles.

The fix here is to simply install FooModule in ChildComponent instead of ParentComponent.

Example #2:

@Component(modules = SkyBlueModule.class)
interface ParentComponent {
  ChildComponent child();
}

@Module
abstract class SkyBlueModule {
  @Binds Blue skyBlue(SkyBlue sky);
}

class SkyBlue implements Blue {
  @Inject SkyBlue(Crayons crayons);
}

@Subcomponent(modules = CrayonsModule.class)
interface ChildComponent {
  Blue blue();
}

@Module
class CrayonsModule {
  @Provides
  Crayons crayons() {
    return Crayons.new128ColorsBox();
  }
}

What's happening here?

ParentComponent installs SkyBlueModule, which assigns a binding for Blue. But the aliased binding, SkyBlue, has a missing dependency in ParentComponent: there's no binding for Crayons!!!

ChildComponent, however, does have a Crayons binding. And neither Blue nor SkyBlue is ever requested in ParentComponent, so the bindings "float down" to ChildComponent.

Note that this can happen transitively, potentially in many layers. We have to walk the dependency tree to figure out what transitive dependency is forcing all the placement of these bindings (here, binding Blue to SkyBlue seems fine, since they could both be resolved in ParentComponent. It's only because Crayons isn't bound in ParentComponent that the binding fails).

There are two possible fixes here: either install CranyonsModule in ParentComponent or install SkyBlueModule in ChildComponent.

How can see these errors for myself?

Compile your code with Dagger 2.17 and take note of the errors that you see. If the dependency trace is not helping you diagnose the problem, save the error output and pass the -Adagger.floatingBindsMethods=enabled flag to javac when recompiling. This will print special warnings that should help in showing what bindings have "moved" from one component to another as a result of the bugfix, and why they have moved. Some of these are safe, but if you correlate the information with the dependency traces that you found, you should have the necessary information to diagnose the problem.

Clone this wiki locally