Using Dagger for dependency injection on construct

2020-05-17 04:35发布

问题:

So, I'm currently redesigning an Android app of mine to use Dagger. My app is large and complicated, and I recently came across the following scenario:

Object A requires a special DebugLogger instance which is a perfect candidate for injection. Instead of passing around the logger I can just inject it through A's constructor. This looks something like this:

class A
{
    private DebugLogger logger;

    @Inject
    public A(DebugLogger logger)
    {
        this.logger = logger;
    }

    // Additional methods of A follow, etc.
}

So far this makes sense. However, A needs to be constructed by another class B. Multiple instances of A must be constructed, so following Dagger's way of doing things, I simple inject a Provider<A> into B:

class B
{
    private Provider<A> aFactory;

    @Inject
    public B(Provider<A> aFactory)
    {
        this.aFactory = aFactory;
    }
}

Ok, good so far. But wait, suddenly A needs additional inputs, such as an integer called "amount" that is vital to its construction. Now, my constructor for A needs to look like this:

@Inject
public A(DebugLogger logger, int amount)
{
...
}

Suddenly this new parameter interferes with injection. Moreover, even if this did work, there would be no way for me to pass in "amount" when retrieving a new instance from the provider, unless I am mistaken. There's several things I could do here, and my question is which one is the best?

I could refactor A by adding a setAmount() method that is expected to be called after the constructor. This is ugly, however, because it forces me to delay construction of A until "amount" has been filled in. If I had two such parameters, "amount" and "frequency", then I would have two setters, which would mean either complicated checking to ensure that construction of A resumes after both setters are called, or I would have to add yet a third method into the mix, like so:

(Somewhere in B):

A inst = aFactory.get();
inst.setAmount(5);
inst.setFrequency(7);
inst.doConstructionThatRequiresAmountAndFrequency();

The other alternative is that I don't use constructor-based injection and go with field-based injection. But now, I have to make my fields public. This doesn't sit well with me, because now I am obligated to reveal internal data of my classes to other classes.

So far, the only somewhat elegant solution I can think of is to use field-based injection for providers, like so:

class A
{
    @Inject
    public Provider<DebugLogger> loggerProvider;
    private DebugLogger logger;

    public A(int amount, int frequency)
    {
        logger = loggerProvider.get();
        // Do fancy things with amount and frequency here
        ...
    }
}

Even still, I'm unsure about the timing, since I'm not sure if Dagger will inject the provider before my constructor is called.

Is there a better way? Am I just missing something about how Dagger works?

回答1:

What you are talking about is known as assisted injection and is not currently supported by Dagger in any automatic fashion.

You can work around this with the factory pattern:

class AFactory {
  @Inject DebugLogger debuggLogger;

  public A create(int amount, int frequency) {
    return new A(debuggLogger, amount);
  }
}

Now you can inject this factory and use it to create instances of A:

class B {
  @Inject AFactory aFactory;

  //...
}

and when you need to create an A with your 'amount' and 'frequency' you use the factory.

A a = aFactory.create(amount, frequency);

This allows for A to have final instances of the logger, amount, and frequency fields while still using injection to provide the logger instance.

Guice has an assisted injection plugin which essentially automates the creation of these factories for you. There have been discussion on the Dagger mailing list about the appropriate way for them to be added but nothing has been decided upon as of this writing.



回答2:

What Jake's post says is perfectly true. That said, we (some of the Google folk who work with Guice and Dagger) are working on an alternative version of "assisted injection" or automatic-factory generation which should be usable by Guice or Dagger or stand-alone - that is, it will generate factory class source code for you. These factory classes will (if appropriate) be injectable as any standard JSR-330 class would. But it is not yet released.

Pending a solution like this, Jake Wharton's approach is advisable.



回答3:

You're having a problem because you are mixing injectables and non injectables in your constructor. The general rules for injection that will save you tons of heartache and keep your code clean are:

  1. Injectables can ask for other injectables in their constructor, but not for newables.

  2. Newables can ask for other newables in their constructor but not for injectables.

Injectables are service type objects, ie objects that do work such as a CreditCardProcessor, MusicPlayer, etc.

Newables are value type objects such as CreditCard, Song, etc.



回答4:

Jake's post is great, but there is more simple way. Google created AutoFactory library for creating factory automatically at compile time.

First, create class A with @AutoFactory annotation and @Provided annotation for injecting arguments:

@AutoFactory
public class A {

    private DebugLogger logger;

    public A(@Provided DebugLogger logger, int amount, int frequency) {
        this.logger = logger;
    }
}

Then library creates AFactory class at compile time. So you need just inject the factory to a constructor of B class.

public class B {

    private final AFactory aFactory;

    @Inject
    public B(AFactory aFactory) {
        this.aFactory = aFactory;
    }

    public A createA(int amount, int frequency) {
        return aFactory.create(amount, frequency);
    }
}


回答5:

I just want to add that years passed after this question has been posted and now there is a library called AssistedInject which has been created by Jake and friends at Square, to solve the exact same problem and is fully compatible with Dagger 2.

You can find it here: https://github.com/square/AssistedInject