Getting Started to Dagger 2 Method Injection, Module, Custom Scope and SubComponent

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

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.di

import android.util.Log

import javax.inject.Inject

class 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.di

import 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.module

import 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.di

import android.util.Log

class 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.component

import 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.module

import 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.module

import 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.

Subcomponent Diagram

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.component

import 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.scope

import 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.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
    fun getLoginComponent():LoginComponent
}
package com.daggerudemy.di.component

import 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.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 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

Check Also

Getting Started to Dagger Multibindings with Android View Model from JetPack

Dagger is a great solution for handling Dependency Injection, Until now we used to provide …