Update: Dagger for Android guide available here.
This is the 2nd part of the Dagger 2 Dependency Injection tutorial series. First article can be found here.
In the last article, we talked about the basics of Dagger 2 and how to use it for simple use cases. Now we are going to talk about scopes and its role in lifecycle and subcomponents.
If your design follows the clean architecture pattern, you may have a structure similar to this:
Activities have their own Presenters or ViewModels, 1 or more Presenters or ViewModels may require a single Interactor and Interactors depends on the data layer. This separation of concern approach makes horizontal levels fairly easy to recognize. We can take advantage of two things here:
- Dependency scopes can be localized
- Lifecycle can be localized
We need localization because we do not want all our dependencies to live as long as the application, and there are cases when we want our dependencies to not share the same state by being the same object.
Dagger 2 provides @Scope as a mechanism to handle scoping. Scoping allows you to “preserve” the object instance and provide it as a “local singleton” for the duration of the scoped component.
In the last tutorial, we discussed a special scope called @Singleton. We agreed that @Singleton allows you to define a component or a dependency as a global object. Well that is not entirely true.
@Singleton, is just another scope. It was just there as the default scoping annotation. It provides your dependencies as a singleton as long as you are using the same component.
Let’s take this slow as I think the concept is not pretty straightforward. First, let us define our own scope.
We define a custom scope by using @Scope. We will create an ApplicationScope that is similar to @Singleton to prove that the latter is just another scope.
First we will create an unscoped component.
We defined a data class Warrior with a name property. This will serve as our dependency. Next we define a module that provides this dependency. But everytime a new warrior is instantiated, we increment the index. This will help us to figure out if indeed a new instance is created every time. Finally a component that exposes this dependency. Then let us instantiate in our application.
Running our app will output these logs:
D/WarriorApplication: Warrior 1
D/WarriorApplication: Warrior 2
This is expected as the component and dependency is unscoped. Now let us annotate our component and provide method with our new scope.
Running our application will now output the following logs
D/WarriorApplication: Warrior 1
D/WarriorApplication: Warrior 1
Notice that the same instance is returned to us. This is the power of scoping. It provides us local singletons within the defined scope.
Now, the scope is only valid within the component. When a new component of the same type is initialized, it will have a new sets of dependencies. This is the reason why @Singleton components are instantiated and maintained in the Application class. The singleton application component should persist throughout the application lifecycle, and the app should refer to this component to uphold that “single instance” requirement.
Following from our architecture design, we can therefore define each layer as a scope.
Now that we have defined the different scopes, we need to find a way to establish a relationship between our components. There are 2 ways and we will discuss them both.
A component can establish a parent-child dependency between itself and other components. For this to work, the parent components should expose their child’s dependency. Let’s take a look at an example.
Suppose you have an app that follows the MVP design pattern (More on MVP here). You want your WarriorActivity to be injected with a WarriorPresenter. This presenter is designed for the WarriorActivity only so we can scope this as @WarriorScreenScope.
First we define a presenter that accepts a warrior instance. Then we define another custom scope @WarriorScreenScope. Next we define a module that provides a warrior presenter. Note that we don’t want to create our own warrior but we want the app component to provide it for us. Now the important part. We establish the dependency to other components using the dependencies property of the component. Multiple components are supported. The only requirement is that your parent components should expose your required dependency, which in our case, the warrior object. Fortunately it is already exposed for us.
fun getWarrior(): Warrior // <- this method in the AppComponent
Now let us instantiate our WarriorScreenComponent and inject our WarriorPresenter to the activity.
First we need to instantiate the parent components and pass their instances in our component. The above example is just to simplify things but the best approach as discussed previously is to save the instance of your global components and use them in you child components. This will allow you to use the scoped objects in those components. Running the activity will output this which is as expected.
D/WarriorActivity: Warrior 1
Subcomponent is another way of building component relationships. This can be thought of as something similar in concept to inner/outer class relationship between classes in OOP (Thanks to Farid Mammadov for pointing this out). A component can only have 1 parent while a parent can be depended to by multiple components. Let’s take a look as how to use subcomponent.
The parent should provide a method to get their child components. The code above exposes the WarriorScreenComponent from the AppComponent. This will allow us to instantiate WarriorScreenComponent from AppComponent.
Now the child component.
The child component is basically the same with the exception of the @Subcomponent annotation. This specifies that this component is a subcomponent. Note that a subcomponent can only have one parent. you cannot specify your parent actually. Your parent (or the parent of its parent) should ensure that it has all its child’s dependency (aside from the modules of course).
Now instantiating is pretty straightforward.
You can get the subcomponents from the parent components (and providing the parameters of course).
That concludes our scopes tutorial. Watch out for future articles regarding dagger 2. Hope you enjoyed this one.