John Kristian
John Kristian

Reputation: 61

Dagger: inject into different instances of one class

Can I use Dagger to inject different values into several instances of the same class that are deep inside the object graph? I want to avoid passing the values through the containing objects in the graph (so I can change the implementation of the contained objects without affecting their containers).

Here's a contrived example. The object graph is a Top, which contains a Left and Right, which each contain a Show. So there are two instances of Show.

class Top {
  Left left;
  Right right;
  void encodeTwice(String data) {
    left.encode(data);
    right.encode(data.getBytes());
  }
}
class Left {
  Leaf leaf;
  void encode(String data) {
    leaf.write(URLEncoder.encode(data));
  }
}
class Right {
  Leaf leaf;
  void encode(byte[] data) {
    leaf.write(DatatypeConverter.printBase64Binary(data));
  }
}
interface Leaf {
  void write(String data);
}
class Show implements Leaf {
  String label;
  @Override public void write(String data) {
    System.out.println(label + ": " + data);
  }
}
// There might be other classes that implement Leaf.

Can I use Dagger to inject different values into Top.left.leaf.label and Top.right.leaf.label?

Here's one attempt. This is unsatisfactory, because Left and Right depend on the implementation of Leaf. I want to inject Show.label without involving Left or Right.

ObjectGraph.create(new TopModule()).get(Top.class).encodeTwice("Hello!");

@Module(injects = Top.class)
public class TopModule {
  @Provides @Named("Left.leaf.label") String provideLeftLabel() {
    return "URL encoded";
  }
  @Provides @Named("Right.leaf.label") String provideRightLabel() {
    return "Base 64";
  }
}
class Top {
  @Inject Left left;
  @Inject Right right;
  void encodeTwice(String data) {
    left.encode(data);
    right.encode(data.getBytes());
  }
}
class Left {
  Leaf leaf;
  @Inject Left(@Named("Left.leaf.label") String label) {
    leaf = new Show(label);
  }
  void encode(String data) {
    leaf.write(URLEncoder.encode(data));
  }
}
class Right {
  Leaf leaf;
  @Inject Right(@Named("Right.leaf.label") String label) {
    leaf = new Show(label);
  }
  void encode(byte[] data) {
    leaf.write(DatatypeConverter.printBase64Binary(data));
  }
}
interface Leaf {
  void write(String data);
}
class Show implements Leaf {
  String label;
  Show(String label) {
    this.label = label;
  }
  @Override public void write(String data) {
    System.out.println(label + ": " + data);
  }
}

Upvotes: 4

Views: 4595

Answers (3)

Dandre Allison
Dandre Allison

Reputation: 6035

Jake has presented great examples of different options. From my understanding, the label is really defined by the Left or Right classes. You can move the field to those classes and pass them to the Leaf. If you want a label-less Leaf you could extend the interface to with LabeledLeaf, then Left and Right could instanceof LabeledLeaf to decide if they should use the labeled write method.

Your sample setup actually sounds a lot like Timber. You should consider seeing how we get Trees that are able to write to different outputs based on their implementation, and some are able to be labeled. A difference in the end result, is that Timber's API is static methods, not a component you inject into classes.

Upvotes: 0

John Kristian
John Kristian

Reputation: 61

Dagger can do this, if you create a separate ObjectGraph for each instance of the injected class. To provide a complete graph, a module can implement @Provides methods that delegate to the subgraphs. Thanks to Jake Wharton for a suggestion that pushed me toward this technique.

Here's an example:

LeafModule leftLeaf = new ShowModule("URL encoded");
LeafModule rightLeaf = new ShowModule("Base 64");
ObjectGraph top = ObjectGraph.create(new TopModule(leftLeaf, rightLeaf));
top.get(Top.class).encodeTwice("Hello!");

@Module(injects = Top.class)
class TopModule {
  TopModule(LeafModule leftLeaf, LeafModule rightLeaf) {
    leftGraph = ObjectGraph.create(leftLeaf).plus(new MiddleModule());
    rightGraph = ObjectGraph.create(rightLeaf).plus(new MiddleModule());
  }
  private final ObjectGraph leftGraph;
  private final ObjectGraph rightGraph;
  @Provides Left getLeft() {
    return leftGraph.get(Left.class);
  }
  @Provides Right getRight() {
    return rightGraph.get(Right.class);
  }
  @Module(injects = {Left.class, Right.class},
          complete = false /* doesn't inject Leaf */)
  static class MiddleModule {
  }
}
/** A Dagger module that can provide a Leaf. */
interface LeafModule {
}
@Module(injects = Leaf.class)
class ShowModule implements LeafModule {
  ShowModule(String showLabel) {
    label = showLabel;
  }
  private final String label;
  @Provides @Named("Show.label") String getLabel() {
    return label;
  }
  @Provides Leaf provideLeaf(Show implementation) {
    return implementation;
  }
}
class Top {
  @Inject Left left;
  @Inject Right right;
  void encodeTwice(String data) {
    left.encode(data);
    right.encode(data.getBytes());
  }
}
class Left {
  @Inject Leaf leaf;
  void encode(String data) {
    leaf.write(URLEncoder.encode(data));
  }
}
class Right {
  @Inject Leaf leaf;
  void encode(byte[] data) {
    leaf.write(DatatypeConverter.printBase64Binary(data));
  }
}
interface Leaf {
  void write(String data);
}
class Show implements Leaf {
  @Inject @Named("Show.label") String label;
  @Override public void write(String data) {
    System.out.println(label + ": " + data);
  }
}

Upvotes: 0

Jake Wharton
Jake Wharton

Reputation: 76075

So this is hard to reason about with such abstract types because based on what Left, Right, and Top actually are I might change approaches.

There are three approaches to this problem that I can think of: a Leaf factory, differentiation of Leafs by qualifier, differentiation of Leafs by child graphs.

Factory

Hide the Leaf implementation behind a factory that takes its required dependencies.

interface LeafFactory {
  Leaf create(String name);
}

class TopModule {
  // ...

  @Provide @Singleton LeafFactory provideLeafFactory() {
    return new LeafFactory() {
      @Override public Leaf create(String name) {
        return new Show(name);
      }
    };
  }
}

class Left {
  private final Leaf leaf;

  @Inject Left(@Named("..") String name, LeafFactory leafFactory) {
    leaf = leafFactory.create(name);
  }

  // ...
}

Qualifier

Right now you are injecting the label via a qualifier annotation. There's no reason you couldn't do the same thing but with Leaf instances.

class TopModule {
  @Provides @Named("..") Leaf provideLeftLeaf() {
    return new Show("URL encoded");
  }

  @Provides @Named("..") Leaf provideRightLeaf() {
    return new Show("Base64 encoded");
  }
}

class Left {
  @Inject Left(@Named("..") Leaf leftLeaf) { .. }
}

Note: You can still inject the label names as arguments to the provideFooLeaf methods if you need that abstraction as well.

Child Graphs

By extending the graph with a "leaf scope" you can create separate parts of your application (e.g., "right", "left") which only have a single Leaf instance.

@Module(addsTo = TopModule.class)
class LeafModule {
  private final String label;

  LeafModule(String label) {
    this.label = label;
  }

  @Provide @Singleton Leaf provideLeaf() {
    return new Show(label);
  }
}

Now we can .plus() this onto the root object graph to get a scope.

ObjectGraph ogRoot = ObjectGraph.create(new TopModule());
// ...
ObjectGraph ogLeft = ogRoot.plus(new LeafModule("URL encoded"));
Leaf left = ogLeft.get(Leaf.class);
ObjectGraph ogRight = ogRoot.plus(new LeafModule("Base64 encoded"));
Leaf right = ogRight.get(Leaf.class);

Like I said, this is hard to generalize to fit the abstract sample app you've outlined. But I hope you can see how child graphs allow you create a specialized version of the graph which is a superset of the "root" graph. By doing this multiple times you can alter the dependencies which are used to inject your types.

Upvotes: 10

Related Questions