GETTING STARTED TO DAGGER 2 METHOD INJECTION, MODULE, CUSTOM SCOPE AND SUBCOMPONENT

Mahmoud Ramadan
9 min readAug 18, 2020

--

Dagger is one of the most popular Dependency Injection Libraries for Java Programming Language and for Android App Development also. If you do not read the first part of this series I recommend you to check it first from here

After finishing this tutorial you will be able to understand the following

  • What is Method Injection
  • Understanding Dagger 2 Module
  • Understanding Custom Scopes

Prerequisites

To be able to get most out of this tutorial you will need:

  • Android Studio 3.2.1 or higher
  • Emulator of phone
  • Kotlin Basics
  • Intermediate level as Android Developer

Clone Repo From Github

Method Injection

Method Injection is the third type of DI and it is very helpful in some scenarios like when we need to enable caching for our login manager. Let’s assume we have Config class that responsible for enabling and Disabling all features in your App like enable/disable caching

So let’s create a new class called Config.kt

package com.daggerudemy.diimport android.util.Logimport javax.inject.Injectclass Config @Inject constructor(){
var isCachEnabled = false
fun enableCache(loginManager: LoginManager){
isCachEnabled = true
Log.d("Config","${this.isCachEnabled}")
}
}

Let’s consume this class in our Login Manager Like this

package com.daggerudemy.diimport android.util.Log
import javax.inject.Inject
class LoginManager @Inject constructor(private val localStore: LocalStore , private val apiService: ApiService){ fun login(username : String , pass:String){
Log.d("LoginManager","login($username , $pass)")
val token = apiService.authenticate(username,pass)
localStore.saveUserToken(token)
}
@Inject
fun enableCache(config: Config){
Log.d("LoginManger","${config.isCachEnabled}")
config.enableCache(this)
}
}

Notice: Method Injection happens after constructor Injection

Now Let’s go to MainActivity.kt and add a new variable of Config class like the following

@Inject
lateinit var config:Config

Then we need to call enable cache Method on the login Manager like the following

loginManager.enableCache(config)

now let’s run the app and see the log to test the method injection is working well

2019-10-11 14:13:48.41013470-13470/com.daggerudemy D/LoginManger: false
2019-10-11 14:13:48.41013470-13470/com.daggerudemy D/Config: true

As you can see the injection of login manager through the method injection is working well

Dagger 2 Module

The Module is one of awesome features in Dagger 2 because it is an optimized solution for Handling third party dependencies like Retrofit, Okhttp, Picasso,..etc. Also, you can reuse these modules in your apps you just need to define them once and you can copy these modules and use them in your different apps.

I will assume I do not own the Local Store class and it is developed by another developer as a library for example and let’s apply module concept but the question now is

How to define a new Dagger 2 module?

First, let’s create a new package called module then define a new class called LocalStoreModule.kt

Then ass @Module Annotation on the top of it then add a new function called provideLocalStore which returns new Instance of LocalStore() and do not forget to add @Provides annotation on the top of the method

package com.daggerudemy.di.moduleimport com.daggerudemy.di.LocalStore
import dagger.Module
import dagger.Provides
@Module
class LocalStoreModule {
@Provides
fun provideLocalStore() = LocalStore()
}

Then we need to change Local Store class to be like this to demonstrate the Module concept

package com.daggerudemy.diimport android.util.Logclass LocalStore {    fun saveUserToken(token: String) {        Log.d("LocalStore", "saveUserToken($token)")    }}

we removed the constructor Injection for the class only, perfect Now let’s go to our login component and add the following

package com.daggerudemy.di.componentimport com.daggerudemy.MainActivity
import com.daggerudemy.di.module.LocalStoreModule
import dagger.Component
@Component(modules = [LocalStoreModule::class])
interface LoginComponent {
fun inject(mainActivity: MainActivity)
}

we link the component with a new module called LocalStoreModule where Login Component can access the Local store dependencies and provide them to the client when it needs them

let’s run to see if our app still behave as previous or not

2019-10-11 14:56:04.73914555-14555/com.daggerudemy D/Config: true
2019-10-11 14:56:04.73914555-14555/com.daggerudemy D/LoginManager:login(ramadan , 123)
2019-10-11 14:56:04.74014555-14555/com.daggerudemy D/ApiService: authenticate(ramadan , 123)
2019-10-11 14:56:04.74014555-14555/com.daggerudemy D/LocalStore:
saveUserToken(wxydldklkd78dsnjuudiiudf)
2019-10-11 14:56:04.74014555-14555/com.daggerudemy D/LoginManger: false
2019-10-11 14:56:04.740
14555-14555/com.daggerudemy D/Config: true

Perfect it is working as the previous

Generated Code for our Login component after adding the Local store Module

// Generated by Dagger (https://google.github.io/dagger)
package com.daggerudemy.di.component;
import com.daggerudemy.MainActivity;
import com.daggerudemy.MainActivity_MembersInjector;
import com.daggerudemy.di.ApiService;
import com.daggerudemy.di.Config;
import com.daggerudemy.di.LoginManager;
import com.daggerudemy.di.LoginManager_Factory;
import com.daggerudemy.di.LoginManager_MembersInjector;
import com.daggerudemy.di.module.LocalStoreModule;
import com.daggerudemy.di.module.LocalStoreModule_ProvideLocalStoreFactory;
import dagger.internal.Preconditions;
public final class DaggerLoginComponent implements LoginComponent {
private final LocalStoreModule localStoreModule;
private DaggerLoginComponent(LocalStoreModule localStoreModuleParam) {
this.localStoreModule = localStoreModuleParam;
}
public static Builder builder() {
return new Builder();
}
public static LoginComponent create() {
return new Builder().build();
}
private LoginManager getLoginManager() {
return injectLoginManager(
LoginManager_Factory.newInstance<>(
LocalStoreModule_ProvideLocalStoreFactory.provideLocalStore(localStoreModule),
new ApiService())); }
@Override
public void inject(MainActivity mainActivity) {
injectMainActivity(mainActivity); }
private LoginManager injectLoginManager(LoginManager instance) {
LoginManager_MembersInjector.injectEnableCache(instance, new Config());
return instance;
}
private MainActivity injectMainActivity(MainActivity instance) { MainActivity_MembersInjector.injectLoginManager(instance, getLoginManager());
MainActivity_MembersInjector.<em>injectConfig</em>(instance, new Config());
return instance;
}
public static final class Builder {
private LocalStoreModule localStoreModule;
private Builder() {}
public Builder localStoreModule(LocalStoreModule localStoreModule) {
this.localStoreModule = Preconditions.checkNotNull(localStoreModule);
return this;
}
public LoginComponent build() {
if (localStoreModule == null) {
this.localStoreModule = new LocalStoreModule();
}
return new DaggerLoginComponent(localStoreModule);
}
}
}

As you can see you can notice some changes like DaggerLoginComponent takes localStroeModule as a parameter because it depends on it. The second change is injectMainActivity

This method has some changes it injects two instance from the login manager and config for our main activity And also if we give getLoginManager a close look we can notice

private LoginManager getLoginManager() {
return injectLoginManager(LoginManager_Factory.newInstance(
LocalStoreModule_ProvideLocalStoreFactory.provideLocalStore(localStoreModule),new ApiService()));
}
And LoginManager_Factory Implements Factory<LoginManager>

Second Usecase for Module

Let assume that the API service is an interface and I need to make different implementation for this interface like I need to make separate Login services for our login module So we will make some changes in API service class

First, we will change it to be interface then we will create a new class called LoginService that implements ApiService Like this

interface ApiService {
fun authenticate(username: String, pass: String): String
}
package com.daggerudemy
import android.util.Log
import com.daggerudemy.di.ApiService
import javax.inject.Inject
class LoginService @Inject constructor() : ApiService {
override fun authenticate(username: String, pass: String): String {
Log.d("ApiService", "authenticate($username , $pass)")
return "wxydldklkd78dsnjuudiiudf"}
}

let’s connect the login component with the new login service module

package com.daggerudemy.di.component
import com.daggerudemy.MainActivity
import com.daggerudemy.di.module.LocalStoreModule
import com.daggerudemy.di.module.LoginServiceModule
import dagger.Component
@Component(modules = [LocalStoreModule::class , LoginServiceModule::class])
interface LoginComponent {
fun inject(mainActivity: MainActivity)
}

if you run the application you can find the same behavior, perfect. Now if you open the DaggerLoginComponent to see the generated code you will find some changes like DaggerLoginComponent now depends on a new object which is LoginService Module

Now let’s go back to our LoginServiceModule. You can see there is a clear redundancy like in provideLoginService Method, the passing parameter is the same as the return So we can make little improvement which is @Bind. You can use @Bind with an abstract method to do the same behavior of @provides but with more optimization like this

package com.daggerudemy.di.moduleimport com.daggerudemy.LoginService
import com.daggerudemy.di.ApiService
import dagger.Binds
import dagger.Module
import dagger.Provides
@Module
abstract class LoginServiceModule {
@Binds
abstract fun bindLoginService(loginService: LoginService) :ApiService
}

we made some refactoring here, first, we changed the class to be abstract class with an abstract method called bindLoginService instead of provideLoginMethod and with @Binds as annotation

Introduction to Dagger2 Scopes

The Scope is the lifetime of the object in memory so if you do not need to recreate instance, again and again, you need to define a custom scope for it. One of the most popular scopes in Dagger is Singelton and this scope provides shared objects through the whole app so we need to reuse this idea for custom scope.

Why Scopes?

Scope provides us a localization and this is very important for layered architecture like clean architecture where we have 3 layers presentation for handling UI, Domain Layer for managing the business and Data Layer for managing the different data sources. So we can make different scopes for each layer and this is very important for the separation of concerns and scalability.

The question now how can I define a custom scope?

Okay, let’s create an example without scope first to see the benefits when we add a custom scope. The idea is very simple we will create app component to give us an instance from App Logger With incremental index like the following:

package com.daggerudemy.di

class AppLogger (val value:String) {
}

then App Module

package com.daggerudemy.di.module

import com.daggerudemy.di.AppLogger
import dagger.Module
import dagger.Provides

@Module
class AppModule {
var index = 0

@Provides
fun getAppLogger():AppLogger {
index++
return AppLogger("index = $index")
}
}

then link the app component with app module

package com.daggerudemy.di.component

import com.daggerudemy.di.AppLogger
import com.daggerudemy.di.module.AppModule
import dagger.Component

@Component(modules = [AppModule::class])
interface AppComponent {
fun getAppLogger():AppLogger
}
package com.daggerudemy

import android.app.Application
import android.util.Log
import com.daggerudemy.di.component.DaggerAppComponent
import com.daggerudemy.di.module.AppModule

class App: Application() {
override fun onCreate() {
super.onCreate()
val appComponent =
DaggerAppComponent.builder().appModule(AppModule()).build()
Log.d("App",appComponent.getAppLogger().value)
Log.d("App",appComponent.getAppLogger().value)
Log.d("App",appComponent.getAppLogger().value)
}
}

now let’s run and see the result

2019-10-12 12:52:52.0723115-3115/com.daggerudemy D/App: index = 1
2019-10-12 12:52:52.0723115-3115/com.daggerudemy D/App: index = 2
2019-10-12 12:52:52.0733115-3115/com.daggerudemy D/App: index = 3

Now you can see the result which has 3 different values for the index which means three different instantiations for the App logger class and this is not memory optimization now how can we solve this situation here custom scope comes to the scene please give a big hand forDagger2 Custom scope

CustomScope

Let’s create a new package under di and called it scope and create a new scope called

package com.daggerudemy.di.scope

import javax.inject.Scope

@Scope
@Retention
annotation class AppScope

then add this scope to the app component and app module method to get an instance from app logger

like this

package com.daggerudemy.di.component
import com.daggerudemy.di.AppLogger
import com.daggerudemy.di.module.AppModule
import com.daggerudemy.di.scope.AppScope
import dagger.Component
@AppScope
@Component(modules = [AppModule::class])
interface AppComponent {
fun getAppLogger():AppLogger
}
package com.daggerudemy.di.moduleimport com.daggerudemy.di.AppLogger
import com.daggerudemy.di.scope.AppScope
import dagger.Module
import dagger.Provides
@Module
class AppModule {
var index = 0
@Provides
@AppScope
fun getAppLogger():AppLogger {
index++
return AppLogger("index = $index")
}
}

now let’s run the app and see the result

2019-10-12 13:10:58.5005605-5605/com.daggerudemy D/App: index = 1
2019-10-12 13:10:58.5005605-5605/com.daggerudemy D/App: index = 1

As you can see you can notice that the index has the same value which means dagger used the first instantiated object from logger and reuse it .

Subcomponent

Subcomponent is a type of component which dives from parent component and inherits its dependencies, subcomponent can have one and only one parent component.

If you look carefully at the previous Diagram you will see the triangles are modules that provide dependencies and circles are components and squares places where they are injected. The darker circle on the top is a parent component and lighter ones are the subcomponents.
Subcomponent does the same as Component but there are some differences. To understand this let’s take the following example, The idea is very simple we need to scale our app where we have a login component that uses the objects in the App component which is our base component so we can do this using component or subcomponent so let’s start.

Scale using Component

We defined AppComponent before and it acts as the base component in our App, Now we need to our login component to depend on AppComponent like the following:

package com.daggerudemy.di.componentimport com.daggerudemy.MainActivity
import com.daggerudemy.di.module.LocalStoreModule
import com.daggerudemy.di.module.LoginServiceModule
import com.daggerudemy.di.scope.ActivityScope
import dagger.Component
@ActivityScope
@Component(dependencies = [AppComponent::class], modules = [LocalStoreModule::class , LoginServiceModule::class])
interface LoginComponent {
fun inject(mainActivity: MainActivity)
}

you can see the login component depends on AppComponent and also there is a new custom scope called ActivityScope which must be added because the login component depends on a scoped component which is AppComponent.

package com.daggerudemy.di.scopeimport javax.inject.Scope@Scope
@Retention
annotation class ActivityScope

And you can use this in your MainActivity like the following :

package com.daggerudemy
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.daggerudemy.di.Config
import com.daggerudemy.di.LoginManager
import com.daggerudemy.di.component.DaggerAppComponent
import com.daggerudemy.di.component.DaggerLoginComponent
import com.daggerudemy.di.module.LocalStoreModule
import javax.inject.Inject
class MainActivity : AppCompatActivity() { @Inject
lateinit var loginManager: LoginManager
@Inject
lateinit var config: Config
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val appComponent = DaggerAppComponent.create() val loginComponent = DaggerLoginComponent.builder()
.appComponent(appComponent)
.localStoreModule(LocalStoreModule()).build()

loginComponent.inject(this)
loginManager.login("ramadan", "123")
loginManager.enableCache(config)
}
}

Scale using SubComponent

To do the same function using Subcomponent you need to use AppComponent directly and add reference to your subcomponent, now you can benefit from all objects in App module

package com.daggerudemy.di.componentimport com.daggerudemy.di.AppLogger
import com.daggerudemy.di.module.AppModule
import com.daggerudemy.di.scope.AppScope
import dagger.Component
@AppScope
@Component(modules = [AppModule::class])
interface AppComponent {
fun getAppLogger():AppLogger
fun getLoginComponent():LoginComponent
}
package com.daggerudemy.di.componentimport com.daggerudemy.MainActivity
import com.daggerudemy.di.module.LocalStoreModule
import com.daggerudemy.di.module.LoginServiceModule
import com.daggerudemy.di.scope.ActivityScope
import dagger.Component
import dagger.Subcomponent
@ActivityScope
@Subcomponent( modules = [LocalStoreModule::class , LoginServiceModule::class])
interface LoginComponent {
fun inject(mainActivity: MainActivity)
}
package com.daggerudemyimport android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.daggerudemy.di.Config
import com.daggerudemy.di.LoginManager
import com.daggerudemy.di.component.DaggerAppComponent
import javax.inject.Inject
class MainActivity : AppCompatActivity() { @Inject
lateinit var loginManager: LoginManager
@Inject
lateinit var config: Config
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerAppComponent.create().getLoginComponent()
.inject(this)
loginManager.login("ramadan", "123")
loginManager.enableCache(config)
}
}

finally, The main difference is the way of passing objects between the dependent modules. Using components gives a programmer more control over which objects may be used in the derivative components. Also important is the fact that we don’t need to add new features to the base component each time, which fits very well with the idea of Open-Closed.
In the case of subcomponents, we immediately have access to all objects of the base component. In most cases, a better practice for our applications would be to use common components. This would make our code cleaner and more resistant to changes. We should use subcomponents in situations when both components are strongly related logically and when a derivative component uses most of the objects of the base component.

Conclusion:

In this tutorial, we learned about Module, scope, and subcomponent form Dagger and we discussed the different ways to scale your app architecture. In the next tutorial, we will talk about Dagger multibindings with View Model

--

--

Mahmoud Ramadan
Mahmoud Ramadan

Written by Mahmoud Ramadan

Android Tech Lead | Software Engineer

No responses yet