Emily L.
Emily L.

Reputation: 5931

Dagger 2 module "interfaces"?

I'm quite new to Dagger 2 and I'm looking for a way to have a "configurable component".

Essentially this is what I want to achieve:

public interface ErrorReporter{
   ...
}

public class ConsoleErrorReporter implements ErrorReporter{
   ... // Print to System.err
}

public class DialogErrorReporter implements ErrorReporter{
   ... // Show modal dialog to user
}


@Module
public interface UIModule{
    @Provides
    ErrorReporter provideErrorReporter();
}

@Module
public class ConsoleUIModule{
    @Override
    @Provides
    ErrorReporter provideErrorReporter(ConsoleErrorReporter cer){
        return cer;
    }
}


@Module
public class GraphicalUIModule{
    @Override
    @Provides
    ErrorReporter provideErrorReporter(DialogErrorReporter  der){
        return der;
    }
}

@Component(modules = {UIModule.class, OtherUniversalModule.class})
public interface ApplicationComponent{
    ErrorReporter errorReporter();
}


void main(String[] args){
    final UIModule uiModule;
    if(args.length == 1 && args[0].equals("gui")){
        uiModule = new GraphicalUIModule();
    }else{
        uiModule = new ConsoleUIModule();
    }

    DaggerApplicationComponentdac = DaggerApplicationComponent.builder()
                                       .uiModule(uiModule).build();
    dac.errorReporter().showError("Hello world!");
}

The above fails with @Provides methods cannot be abstract unfortunately both for interfaces and abstract classes. I have also tried non-abstract base class with concrete implementations that return null and then overriding these in sub classes. However this also fails with @Provides methods may not override another method.

In short I want to define a contract for a module and choose different modules during runtime. I know that Dagger 2 compile time validates the object graph, but if I have a well defined contract that should still be possible right? Or am I forced to create two different components with duplicate code for both user interfaces? Are there other solutions that I'm missing?

Upvotes: 4

Views: 3087

Answers (1)

David Medenjak
David Medenjak

Reputation: 34532

I don't think using a module this way is possible, because...

Suppose you have the following two constructors for your classes

@Inject ConsoleErrorReporter(Console console);

@Inject DialogErrorReporter(Graphics graphics);

This would mean that ConsoleUIModule would require a Console and DialogErrorReporter would require a Graphics object to create their respecitve implementation of ErrorReporter.

But if dagger only knows about UIModule because you use the interface there...well...it could not provide the dependencies for either, because it doesn't know about any of them.

And if you don't know the dependencies building a dependency graph at compile time won't work. Also this won't compile even without dagger because provideErrorReporter(ConsoleErrorReporter cer) does not override provideErrorReporter().


What you can and should do is use different components. Because a component is the thing that actually knows how to provide things. And a component already is an interface—and that's what you wanted, right?

You can have component dependencies, where one component depends on another. E.g. have a DependentComponent that provides a NeedsErrorReporter that needs an implementation of ErrorReporter. We also depend on an interface, rather than the actual component (and that's what you wanted after all, right?)

You then implement the interface by actual components, and each component has its respective modules (and maybe even further dependencies). In the end you have a component that you can switch and will provide different versions of an object, properly encapsulated!

@Component(dependencies = UIComponent.class) /* <- an interface! */
interface DependentComponent {
  NeedsErrorReporter needsErrorReporter();
}

class NeedsErrorReporter {
  @Inject public NeedsErrorReporter(ErrorReporter reporter) { }
}


/* this is _not_ a component, but a simple interface! */
interface UIComponent {
  ErrorReporter errorReporter();
}


/* Console */
@Component(modules = ConsoleUIModule.class)
interface ConsoleUIComponent extends UIComponent { }

@Module interface ConsoleUIModule {
  @Binds ErrorReporter provideErrorReporter(ConsoleErrorReporter cer);
}

/* Graphic */
@Component(modules = GraphicalUIModule.class)
interface GraphicUIComponent extends UIComponent { }

@Module interface GraphicalUIModule {
  @Binds ErrorReporter provideErrorReporter(DialogErrorReporter der);
}

/* The error reporter variants */
interface ErrorReporter {
}

class ConsoleErrorReporter implements ErrorReporter {
  @Inject public ConsoleErrorReporter() { }
}

class DialogErrorReporter implements ErrorReporter {
  @Inject public DialogErrorReporter() { }
}

Now all you have to do is pick the right component ;)

DaggerDependentComponent.builder().uIComponent(DaggerConsoleUIComponent.create()).build();
    // or
DaggerDependentComponent.builder().uIComponent(DaggerGraphicUIComponent.create()).build();

Upvotes: 5

Related Questions