jneander jneander - 6 months ago 18
Android Question

Nested/Recursive Injections with Dagger

When using Dagger, what approaches will allow for free/easy instantiation of @Inject fields on objects that are also instantiated through injection.

For example, the code below will inject an object of type Bar into a given Foo object. It will do this in either of the two ways displayed. However, the Sly field of each Bar object is not matching that behavior.

Foo

public class Foo {
@Inject Bar bar;

public String getValue() {
return "Foo's bar value: " + bar.getValue();
}
}


Bang

public class Bar {
@Inject Sly sly;

public String getValue() {
return "Bar's sly value: " + sly.getValue();
}
}


Sly

public class Sly {
public String getValue() {
return "Hey!";
}
}


Module

@Module(
injects = {
Foo.class,
Bar.class
}
)
public class ExampleTestModule {
@Provides
Bar provideBar() {
return new Bar();
}

@Provides
Sly provideSly() {
return new Sly();
}
}


Tests

public void testWorksWithInject() {
Foo foo = new Foo();
ObjectGraph.create(new ExampleTestModule()).inject(foo);
assertEquals("...", foo.getValue()); // NullPointerException
}

public void testWorksWithGet() {
Foo foo = ObjectGraph.create(new ExampleTestModule()).get(Foo.class);
assertEquals("...", foo.getValue()); // NullPointerException
}


In either case, Bar's Sly is not being instantiated/@Injected. Of course, Dagger allows for constructor injection, which solves the problem. I would like to know if there are alternatives to tucking those classes into the parameter list of constructors. What works well for you?

Answer

So the problem here is that Bar has @Inject Sly on it, but then you provide Bar in an @Provides method. @Provides methods override default instantiation behaviour, so you are telling Dagger to instantiate "new Bar()" and return it as a fulfillment of Bar's provision.

The easiest thing you can do is simply delete the provideBar() method, as it is unnecessary. If a concrete type has an @Inject constructor or an @Inject field, Dagger will inject its dependencies and create it, unless it has an inaccessible constructor or a parameterized constructor that has no @Inject. But your case above of class Bar{} above is entirely suitable for implicit binding, without use of @Provides methods.

If you need to alter that default behaviour for some reason, you can still create it in an @Provides method, but you must manually pass in the injected value. @Provides methods, however, can themselves be injected by adding parameters to the @Provides method itself. So you could do this.

@Provides
Bar provideBar(Sly sly) {
  Bar bar = new Bar();
  bar.sly = sly;
  return bar;
}

A @Provides method takes all responsibility for properly provisioning the instance, including newing, assignments, any initialization logic, etc.

But, given your example above, the easy solution is to simply delete provideBar() from your module and let Dagger automatically initialize Bar.

There are some different alternatives which Dagger 2 seems to favor for nested injection: ask for a component, MembersInjector, or give Bar an @Inject annotated constructor.

If Bar has an @Inject annotated constructor then you can achieve full immutability:

class Bar {
  private final Sly sly;

  @Inject
  public Bar(Sly sly) {
    this.sly = sly;
  }
}

The other alternative when you are only partially injecting members is to use a @Provides method with a MembersInjector or component (MembersTestComponent?) as a method argument:

@Provides
Bar provideBar(MembersInjector<Bar> injector) {
  Bar bar = new Bar();
  injector.inject(bar);
  return bar;
}

Providing the MembersTestComponent argument will unfortunately couple the Module back to your Component and make the solution less cohesive. Providing the MembersInjector is especially useful if Bar contains scoped values provided by the Component (e.g. user inside Tweeter in Jake Wharton's Devoxx 2014 talk).