Search code examples
androidkotlindagger-2

Circle features dependency with subcomponents


In my current Android project I have a feature A that display feature B, and now I need to be able to display feature A from feature B. Which create a circle feature dependency, generating a StackOverflow error on build time.

@Subcomponent(modules = [SubComponentA.Module::class])
interface SubComponentA {

    fun plus(module: Module): SubComponentB

    @dagger.Module
    class Module {
        // Provide stuff
    }
}

-------------

@Subcomponent(modules = [SubComponentB.Module::class])
interface SubComponentB {

    fun plus(module: Module): SubComponentA

    @dagger.Module
    class Module {
        // Provide stuff
    }
}

Is there a way to achieve this Dagger graph without a build time error? Thanks!


Solution

  • If A produces B and B produces A, I imagine it would be difficult to get an instance of either one to act as the other's parent. That's not a problem, though: Dagger components do not have to represent the exact same ownership and access chain that your model objects or application UI represents. The important part of the Dagger graph is whether the objects you want are directly injectable and whether they have the correct Dagger-managed lifetime ("scope").

    You clarified in the comments:

    To add more context: Feature A is an article that can open another article or a Feature B, which is a detail view of a Hike. Inside the Feature B (Hike detail) we can access to an article (Feature A) and so on.

    If the Article and Hike aren't directly related to each other in a nesting or ownership sense—you might start the app and navigate directly to either Articles or Hikes—then I would have the main Component act as the owner of both Subcomponents, such that neither Subcomponent is the parent of the other. Because Subcomponents can access all the bindings of their parent component tree, you'll be able to inject a SubcomponentA builder/factory1 from Component, SubcomponentA, or SubcomponentB, and you'll likewise be able to inject a SubcomponentB builder/factory from Component, SubcomponentA, or SubcomponentB. You won't be able to get to SubcomponentA bindings from SubComponentB (i.e. get to Article subcomponent Dagger bindings from the Hike subcomponent) or vice versa, but of course you can use a Module field or @BindsInstance binding to pass details about the Article or Hike you just navigated from. You could even pass the subcomponent instance itself, but in your position I'd probably just keep data model objects or identifiers to avoid keeping a long memory-expensive chain of objects.

    If it is the case that Articles have zero or more Hikes and every Hike has exactly one Article, and that the Hike has reason to directly access all the Dagger bindings ("ArticleInteractionLogger", maybe) associated with its parent Article, then that's a good reason that SubcomponentB would be a subcomponent of SubcomponentA. However, then you won't be able to get to a Hike (SubcomponentB) instance without first getting an Article (SubcomponentA) instance, and navigating to a different Article means you would not inject the bindings directly from the Hike subcomponent you were just in.

    All that said, it sounds like your motivation for subcomponents is cross-navigation, in which case I'd just leave the Dagger object graph out of it, have both Subcomponents installed on the parent Component, and save the history elsewhere—as subcomponent @BindsInstance fields or in a separate NavigationHistoryManager class of your own design.


    Note 1: You're using the plus abstract factory method model from Dagger 1, but it is more idiomatic to define a Builder or Factory that you can directly inject. This avoids having to keep or inject the Component or Subcomponent instance directly to get to the plus method (which could be named anything). However, to use this you'll need to specify the Subcomponent in the subcomponents attribute of the @Module annotation for a Module on your parent Component.

    @Subcomponent(modules = [SubComponentA.Module::class])
    interface SubComponentA {
    
        // Remove: fun plus(module: Module): SubComponentB
    
        @dagger.Module class Module { /* ... */ }
    
        @Subcomponent.Factory
        interface Factory {
            fun create(module: Module): SubComponentA
        }
    }
    
    @Subcomponent(modules = [SubComponentB.Module::class])
    interface SubComponentB {
    
        // Remove: fun plus(module: Module): SubComponentA
    
        @dagger.Module class Module { /* ... */ }
    
        @Subcomponent.Factory
        interface Factory {
            fun create(module: Module): SubComponentB
        }
    }