cobby
cobby

Reputation: 498

How to lazy-inject interfaces with Dagger-2?

I came across an instance where Dagger-2 wouldn't let me lazy inject. It seems that it still requires me to supply the object at compile time. Why is that?

The stacktrace:

[Dagger/MissingBinding] @javax.inject.Named("htfModel") de.wimj.core.Applications.IModel cannot be provided without an @Provides-annotated method.
[ERROR]       @javax.inject.Named("htfModel") de.wimj.core.Applications.IModel is injected at
[ERROR]           de.wimj.ui.Mt5Painter.<init>(…, htfTradeModel, …)
[ERROR]       dagger.Lazy<de.wimj.ui.Mt5Painter> is injected at
[ERROR]           de.wimj.core.Applications.ModelMqlBased.<init>(…, mt5Painter, …)
[ERROR]       dagger.Lazy<de.wimj.core.Applications.ModelMqlBased> is injected at
[ERROR]           de.wimj.di.components.trademodel.ModelModule.iModel(modelMqlBased, …)
[ERROR]       de.wimj.core.Applications.IModel is provided at
[ERROR]           de.wimj.di.components.trademodel.ModelComponent.createModel()

The code for the stacktrace:

//Got it, Dagger-2 wants me to provide a IModel here
@ModelScope
@Component(modules = { ModelModule.class }, dependencies = { ClientComponent.class })
public interface ModelComponent {

    IModel createModel();

    @Component.Builder
    interface Builder {
        ModelComponent build();
        Builder clientComponent(ClientComponent clientComponent); //MT5Trader comes from this component
    }

}


//At this point I will provide the IModel. I do NOT get, why Dagger-2 forces
//me to provide a "ModelMqlBased" though. I obviously lazy-inject it. 
//I used this pattern in other cases as well (providing an interface and 
//lazy-injecting the possible instantiations as params)
@Module
public class ModelModule {

    @Provides
    @ModelScope
    IModel iModel(  Lazy<ModelMqlBased> modelMqlBased,  //lazy-injection here!
            ModelFileBased modelFileBased,
            @Named("configClientType")String clientType) {
        switch (clientType) {
        case "mqlBot": 
            return modelMqlBased.get();
        case "fileBot":
                return modelFileBased;
        default:
            throw new RuntimeException();
        }
    }
}

The following code should be irrelevant (the crux is the ModelModule), but for the sake of completion:

@ModelScope
public class ModelMqlBased implements IModel {

    @Inject
    public ModelMqlBased( Lazy<Mt5Painter> mt5Painter) {
        super();
        this.mt5Painter = mt5Painter.get();
    }

}

//this one sits in a "higher-scoped" component
@ClientScope
public class Mt5Painter {

    private IModel htfModel;
    private IModel ltfModel;

    @Inject
    public Mt5Painter(@Named("htfModel") Lazy<IModel> htfTradeModel, @Named("ltfModel") Lazy<IModel> ltfTradeModel) {
        super();
        this.htfModel = htfTradeModel.get();
        this.ltfModel = ltfTradeModel.get();
    }

Upvotes: 0

Views: 2764

Answers (1)

Jeff Bowman
Jeff Bowman

Reputation: 95634

Lazy<T> doesn't mean "figure out later whether T is bound", it means "ensure a binding exists for T at compile time, but only create an instance at runtime after I call get". You will still need to make the binding for T available in all cases, but Dagger won't try to create an instance of it until you explicitly ask for it.

Dagger requires that for all uses of Provider<T> and Lazy<T>, the binding T needs to exist at compile time, even if at runtime you do not call it. This ensures that if you do call get() on the Provider or Lazy instance, it does not fail at runtime for a binding it knew was missing at compile time. (Lazy behaves exactly as Provider does, except Lazy remembers the instance it returns regardless of whether the binding was scoped.)

This means that one of your options is to add a binding for ModelMqlBased that returns null or throws an Exception, which normally would be a terrible idea in Dagger, but it would be sufficient for a case where you know at runtime that the Provides method is never called.

A different way to achieve the flexibility you're looking for is with @BindsOptionalOf. This allows you to inject an Optional<T> or Optional<Lazy<T>>, which resolves to a present value if the binding exists and an absent placeholder if the binding does not.

@Module
public abstract class ModelModule {
    // Note abstract class and static/abstract methods.

    @BindsOptionalOf
    abstract ModelMqlBased bindOptionalOfModelMqlBased();

    @Provides
    @ModelScope
    static IModel iModel(Optional<ModelMqlBased> modelMqlBased,
            ModelFileBased modelFileBased,
            @Named("configClientType")String clientType) {
        switch (clientType) {
        case "mqlBot": 
            return modelMqlBased.get();
        case "fileBot":
            return modelFileBased;
        default:
            throw new RuntimeException();
        }
    }
}

This may make it easier to reuse modules, especially because (like multibindings) you can supply as many @BindsOptionalOf abstract T bindOptionalOfT(); methods as you'd like and Dagger will not complain about the duplication.

Upvotes: 2

Related Questions