Search code examples
dagger

Infinite recursion when using subcomponent for encapsulation


I'm trying to achieve encapsulation by using subcomponent which is described here, but I got infinite recursion.

Here is my code:

//tried adding @ScopeA, still the same.
public class A {
    @Inject
    A(B b) {

    }
}

@ScopeA
public class B {
    @Inject
    B() {

    }
}
@Component(modules = AModule.class)
@Singleton
public interface AComponent {
    public A a();
}
@Module(subcomponents = SComponent.class)
class AModule {
    @Provides
    @Singleton
    A a(SComponent.Factory factory) {
        return factory.component().a();
    }
}
@Subcomponent
@ScopeA
interface SComponent {
    @ScopeA
    A a();
    @Subcomponent.Factory
    interface Factory {
        SComponent component();
    }
}
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DaggerAComponent.create().a();
    }
}

After checking generated dagger code, I found this:


  private final class SComponentImpl implements SComponent {
    private SComponentImpl() {}

    @Override
    public A a() {
      return DaggerAComponent.this.aProvider.get();
    }
  }

It seeems that SComponent are getting A from parent component, which is not what I wanted, where is the problem of my code?


Solution

  • Note that the example from the Subcomponents for Encapsulation page uses a qualifier annotation, @PrivateToDatabase, which is not a scoping annotation and which distinguishes the binding of Database from the binding of @PrivateToDatabase Database.

    Subcomponents inherit all of the bindings from their parent components, so you currently do have A available from the parent component and also A available from the subcomponent. This is especially tricky if anything in your subcomponent needs to inject A, if it weren't marked @Singleton: Do you want the A from the parent component, or the A from the subcomponent?

    Another tricky part of this situation is that you can't use qualifier annotations on classes that use @Inject constructors.

    I'd recommend that you do the following:

    1. Extract an interface from A, so then you have A and AImpl.
    2. Keep your @Provides method that gets an A instance from the subcomponent.
    3. Have the subcomponent expose AImpl, and (to best avoid ambiguity) only inject AImpl in the classes in your subcomponent, not A.

    If you'd rather not extract an interface, you could also work around this problem by removing @Inject from A and writing a @Provides method in a module in the subcomponent that returns a qualified A, so the unqualified A goes through the top-level component and the qualified A is only available within the subcomponent.