I have complex multi-tier architecture in my Android project.
Currently i want to use the following structure of the DI components and modules:
[Data Layer]
@DataScope //scope is used for caching (Singleton) some Data Layer entities for whole application
- DataComponent //exposes just interfaces which should be used on the BL Layer
//Modules exposes entities for internal (Data Layer) injections and entities which exposed by DataComponent for BL Layer
* DataModule1
* DataModule2
* DataModule3
[Business Logic Layer] (also has component dependency on DataComponent)
@BlScope //scope is used for caching (Singleton) some BL Layer entities for whole application
- BlComponent //exposes just interfaces which should be used on the Service Layer & Presentation Layer
//Modules exposes entities for internal (BL Layer) injections and entities which exposed by BLComponent for the Service Layer & Presentation Layer
* BlModule1
* BlModule2
[Service Layer] (also has component dependency on BlComponent) - this layer has Android specific entities (Android Service, ContentProvider) not related to the Presentation Layer
@ServiceScope //scope is used for caching (Singleton) some Service Layer entities for whole application
- ServiceComponent //exposes just interfaces which should be used on the Presentation Layer
* ServiceModule //Module exposes entities for internal (Service Layer) injections and entities which exposed by ServiceComponent for the Presentation Layer
[Presentation Layer] (also has component dependency on: ServiceComponent, BlComponent)
@PresentationScope //scope is used for caching (Singleton) some Presentation Layer entities for whole application
- PresentationComponent //exposes just interfaces which should be used on the current layer
* PresentationModule //Module exposes entities injections on the current layer
The ServiceComponent & BlComponent don't expose the similar interfaces.
To build the main graph i use the following code:
DataComponent dataComponent = DaggerDataComponent.builder().<addModules>.build();
BlComponent dataComponent = DaggerBlComponent.builder().<addModules>.dataComponent(dataComponent).build();
ServiceComponent serviceComponent = DaggerServiceComponent.builder().<addModule>.blComponent(blComponent).build();
PresentationComponent presentationComponent = DaggerPresentationComponent.builder().<addModule>.blComponent(blComponent).serviceComponent(serviceComponent).build();
In the PresentationLayer i use only "presentationComponent" to provide required dependencies from ServiceComponent/Layer and BLComponent/Layer.
Currently scenario above doesn't work, because PresentationComponent depends on the 2 scoped components with the error
"...depends on more than one scoped component:..."
Though it allows to use one scoped component with many non-scoped components. This architecture is directed to restrict to use internal layer entities on the upper layers and in the same time to have independent tests (unit & instrumentation) on each layer/module.
Could anybody help me to understand is it bug or desirable behavior of the Dagger processor? (and why?)
Issue on the Dagger2 repo: https://github.com/google/dagger/issues/747#issuecomment-303526785
It's not a bug, it's a feature
The problems that would emerge from allowing mixing scopes in the manner you have suggested are similar to the problems of multiple inheritance in OO languages.
Consider the following component structure (the diamond problem):
A
/ \
B C
\ /
D
D
sub-component of both B
and C
. Since siblings B
and C
are independent of each other they are free to expose an arbitrary dependency Foo
to sub-components. Assume that Foo
is required for the module set in component D
. Which Foo
should D
now use? The one from B
or the one from C
?
The situation is even more complex if you allow scopes. A scope in Dagger 2 is for marking lifecycle. In other words, it says "I will maintain a reference to these bindings at this point". Hence, your @Singleton
scoped bindings must be grouped in a Component for which you maintain a reference at the level of your Application subclass. It does nothing, for instance, to initialise a @AppScoped
Component as a local variable within a method or as a field of an Activity - it will be discarded with the method stack or destroyed when the Activity is destroyed.
Returning to the above example, assume that B
and C
have different scopes. The Foo
bound in B
is @BScoped
. So it should only live as long as B
. But it is possible for D
to get the @CScoped Foo
from C
. So we can no longer ascertain that B
is exposing a @BScoped Foo
to its subcomponent - we have allowed the possibility of Foo
to escape its scope. This is in addition to the processing problems presented by not restricting ourselves to directed acyclic graphs. As you are probably aware, many problems in computer science become easier to solve once we disallow cycles.
The fact that you get the error message for your set up only when you add scope annotations and it disappears when you remove them is not indicative that there is a bug in Dagger 2 or something lacking. It simply means that without scope annotations you only have the illusion of a Component hierarchy since all Components are effectively equal. Again, even if you think you have somehow made a diamond through combination of non-scoped and scoped components, it will not truly be a diamond because there is no real hierarchy in the non-scoped Components.
Your proposed architecture is well-intentioned in that you are trying to achieve separation of layers. However, Dagger 2 components are for grouping lifecycle, not layers. To illustrate, most Android apps will have app-scoped dependencies, such as a GSON
, a SharedPreferences
etc. These dependencies that live and die together and last for the whole lifecycle of your app are normally bound in the module set for a @Singleton
component. You could also call it @PerApp
or @AppScoped
- it really doesn't matter as long as you retain a reference to the component in your sub-class of Application
.
Underneath this in your directed acyclic graph (the DAG in DAGger) you can put what you want. Some people make a @UserScope
which lasts just as long as the app has a signed-in user. Others skip this step and move straight to @PerActivity
or @ActivityScoped
components. The module sets for the @PerActivity
components should bind members that have the lifecycle of a single activity e.g., the Activity's Context.
At this point it's better to group by functionality like in the Google Android Architecture Blueprint. Although your components at this level will inject dependencies from different layers (the model layer and the presentation layer) these can be separated by judicious use of modules. Additionally, you can group the members will be in the same Java package which means you can use access modifiers to fulfil Effective Java Item 13: Minimise the accessibility of classes and members.