DEV Community

Cover image for Dagger the easy way - Part 2
Harsh Shandilya
Harsh Shandilya

Posted on • Originally published at msfjarvis.dev

Dagger the easy way - Part 2

Welcome back! In this post I'm taking a bit of detour from my planned schedule to write about scoping. We'll definitely cover constructor injection in the next part :)

All the code from this post is available on GitHub: msfjarvis/dagger-the-easy-way

Dagger 2 provides @Scope as a mechanism to handle scoping. Scoping allows you to keep an object instance for the duration of your scope. This means that no matter how many times the object is requested from Dagger, it returns the same instance.

Default scopes

In the previous tutorial, we looked at two scopes, namely @Singleton and @Reusable. Singleton does what its name suggests, and "caches" the dependency instance for the lifecycle of the @Component, and Reusable tells Dagger that while we'd prefer that a cached instance be used, we're fine if Dagger needs to create another one. The new Dagger 2 user guide does a pretty good job differentiating between Singleton, Reusable and unscoped dependencies which I'll reproduce here.

// It doesn't matter how many scoopers we use, but don't waste them.
@Reusable
class CoffeeScooper {
  @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
  @Provides
  // DON'T DO THIS! You do care which register you put your cash in.
  // Use a specific scope instead.
  @Reusable
  static CashRegister badIdeaCashRegister() {
    return new CashRegister();
  }
}

// DON'T DO THIS! You really do want a new filter each time, so this
// should be unscoped.
@Reusable
class CoffeeFilter {
  @Inject CoffeeFilter() {}
}
Enter fullscreen mode Exit fullscreen mode

Why do we need scopes

I'll do a small demo to show the difference between unscoped and singleton dependencies, then we'll move on to defining our own scopes.

// AppComponent.kt

data class Counter(val name: String)

@Component(modules = [AppModule::class])
interface AppComponent {
  fun getCounter(): Counter
}

@Module
class AppModule {
  private var index = 0

  @Provides
  fun provideCounter(): Counter {
    index++
    return Counter("Counter $index")
  }
}
Enter fullscreen mode Exit fullscreen mode

These dependencies are all unscoped, along with the AppComponent. Knowing what we do about unscoped elements in a Dagger graph, predict the output of the following code:

class CounterApplication : Application() {
  private val TAG = "CounterApplication"

  override fun onCreate() {
    super.onCreate()
    val appComponent = DaggerAppComponent.builder()
      .appModule(AppModule())
      .build()
    Log.d(TAG, appComponent.getCounter().name)
    Log.d(TAG, appComponent.getCounter().name)
  }
}
Enter fullscreen mode Exit fullscreen mode

Running this on a device will print the following in your logcat

D/CounterApplication: Counter 1
D/CounterApplication: Counter 2
Enter fullscreen mode Exit fullscreen mode

Totally expected, because unscoped dependencies have no lifecycle in the component, and hence are created every time you ask for one. Let's make them all into Singletons and see how that changes things.

 data class Counter(val name: String)

+@Singleton
 @Component(modules = [AppModule::class])
 interface AppComponent {
   fun getCounter(): Counter
@@ -12,6 +13,7 @@ class AppModule {
   private var index = 0

   @Provides
+  @Singleton
   fun provideCounter(): Counter {
     index++
     return Counter("Counter $index")
Enter fullscreen mode Exit fullscreen mode

Running the same code again, we get

D/CounterApplication: Counter 1
D/CounterApplication: Counter 1
Enter fullscreen mode Exit fullscreen mode

Notice that we were handed the same instance. This is the power of scoping. It lets us have singletons within the defined scope.

Like Arun mentioned in the additional notes for the previous article, ensuring a singleton Component stays that way is the user's job. If you initialize the component again within the same scope, the new component instance will have a new set of instances. That is part of why we store our component in the Application class, because it is the singleton for our apps.

Creating our own scopes

In its most basic form, a scope is an annotation class that itself has two annotations, @Scope and @Retention. Assuming we follow an MVP architecture (purely for nomenclature purposes, scoping is not necessarily tied to your architecture), let's create a scope for our CounterPresenter.

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class CounterScreenScope
Enter fullscreen mode Exit fullscreen mode

Putting this annotation together with our presenter and our component, we finally get this:

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class CounterScreenScope

data class Counter(val name: String)
class CounterPresenter(val counter: Counter)

@Module
class CounterScreenModule {
  @Provides
  @CounterScreenScope
  fun provideCounterPresenter(counter: Counter): CounterPresenter {
    return CounterPresenter(counter)
  }
}

@CounterScreenScope
@Subcomponent(modules = [CounterScreenModule::class])
interface CounterScreenComponent {
  fun inject(counterActivity: MainActivity)
}

@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
  fun counterScreenComponent(counterScreenModule: CounterScreenModule): CounterScreenComponent
}

@Module
class AppModule {
  private var index = 0
  @Provides
  fun getCounter(): Counter {
    index++
    return Counter("Counter $index")
  }
}
Enter fullscreen mode Exit fullscreen mode

Phew, a lot happened there. Let's break it down.

class CounterPresenter(val counter: Counter)
Enter fullscreen mode Exit fullscreen mode

This is simply a class that represents our presenter. We don't care much for implementation details here, so the class does nothing.

@Module
class CounterScreenModule {
  @Provides
  @CounterScreenScope
  fun provideCounterPresenter(counter: Counter): CounterPresenter {
    return CounterPresenter(counter)
  }
}
Enter fullscreen mode Exit fullscreen mode

CounterScreenModule holds the provider method for our presenter. The method is annotated with @CounterScreenScope to indicate that we want to scope its lifetime to our screen. Rather than being an object like our AppModule, it's a class because we need to instantiate it manually later.

@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
  fun counterScreenComponent(counterScreenModule: CounterScreenModule): CounterScreenComponent
}
Enter fullscreen mode Exit fullscreen mode

To our AppComponent, we've simply added a method to provide the CounterScreenComponent.

@CounterScreenScope
@Subcomponent(modules = [CounterScreenModule::class])
interface CounterScreenComponent {
  fun inject(counterActivity: MainActivity)
}
Enter fullscreen mode Exit fullscreen mode

CounterScreenComponent is a Subcomponent. In simple, OOP terms, it's a Component that inherits from another Component. A Subcomponent can only have one parent, and the Subcomponent doesn't get to pick who, much like real life :P

The parent Component is responsible for ensuring that all the dependencies of a Subcomponent are available, other than modules.

Putting it all together

After setting up our Dagger graph, instantiating everything becomes pretty easy.

class MainActivity : AppCompatActivity() {

  @Inject
  lateinit var presenter: CounterPresenter

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      val appComponent = DaggerAppComponent.builder()
        .appModule(AppModule())
        .build()

      val counterScreenComponent = appComponent
        .counterScreenComponent(CounterScreenModule())
      counterScreenComponent.inject(this)
      Log.d(TAG, presenter.counter.name)
  }

  companion object {
    private const val TAG = "MainActivity"
  }
}
Enter fullscreen mode Exit fullscreen mode

Thanks to how our graph is laid out, it is very easy to get subcomponent instances from our parent components.

Alternative initialization

We can also use a @Subcomponent.Factory for CounterScreenComponent to initialize it in a fashion similar to our AppComponent from the previous part. The diff from this change goes something like this:

diff --git app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
index 4271d151da6e..425e8358902c 100644
--- app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
+++ app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
@@ -23,7 +23,8 @@ class MainActivity : AppCompatActivity() {
             .build()

         val counterScreenComponent = appComponent
-            .counterScreenComponent(CounterScreenModule())
+            .counterScreenComponentFactory
+            .create(CounterScreenModule())
         counterScreenComponent.inject(this)
         Log.d(TAG, presenter.counter.name)
     }
diff --git app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
index 2fb831771ee8..72acea6f6f43 100644
--- app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
+++ app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
@@ -1,5 +1,6 @@
 package dev.msfjarvis.daggertutorial.di

+import dagger.BindsInstance
 import dagger.Component
 import dagger.Module
 import dagger.Provides
@@ -28,12 +29,16 @@ class CounterScreenModule {
 @Subcomponent(modules = [CounterScreenModule::class])
 interface CounterScreenComponent {
     fun inject(counterActivity: MainActivity)
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(@BindsInstance counterScreenModule: CounterScreenModule): CounterScreenComponent
+    }
 }

 @Singleton
 @Component(modules = [AppModule::class])
 interface AppComponent {
-    fun counterScreenComponent(counterScreenModule: CounterScreenModule): CounterScreenComponent
+    val counterScreenComponentFactory: CounterScreenComponent.Factory
 }

 @Module
Enter fullscreen mode Exit fullscreen mode

Closing Notes

That's it for this tutorial! Scoping is a rather complex concept, and it took me a long (really, really long) time to grasp its concepts and put this together. Its perfectly fine to not understand it immediately, take your time, and refer to one of the reference articles that I used (listed below) to see if maybe their explanations work better for you. Dagger away!

References

Top comments (0)