Understanding Scopes in DI: (Day 10)

In our last blog post, we understood the use case of a @singleton scope and how it helped us create only 1 instance of an object and reuse it multiple times into our scope.

Now, before we move further, we first need to understand what "scope" really means in the context of Dagger2.

In Dagger 2, the term "scope" is used to control the lifespan of instances created by the framework. Scoping in Dagger 2 ensures that the same instance of a dependency is reused within a particular scope, rather than creating a new instance every time the dependency is requested. Here's a breakdown of what "scope" means in the context of Dagger 2:

  1. Singleton Scope: This is the most common scope. When a component or a module is annotated with @Singleton, Dagger ensures that only a single instance of that dependency exists within the component's lifecycle. This means that every time the dependency is requested, the same instance is returned.

  2. Custom Scopes: Apart from the built-in @Singleton scope, Dagger 2 allows you to define custom scopes. This is useful when you want to scope dependencies to specific parts of your application, such as a logged-in session or a particular feature flow. For example, you might have a @PerActivity scope that ensures a single instance of dependency for each activity.

  3. Component Scopes: In Dagger 2, components can also have scopes. When a component is scoped, it means that the component and its dependencies share the same lifecycle. For instance, if you have a @PerActivity scoped component, all dependencies provided by that component will be scoped to the lifecycle of the associated activity.

  4. Subcomponents and Scopes: Subcomponents can inherit and extend the dependencies of their parent components. However, subcomponents cannot reuse the same scope as their parent. This means if the parent component is @Singleton scoped, the subcomponent must have a different scope or be unscoped.

  5. Unscoped Dependencies: If a dependency is not annotated with any scope, then a new instance of that dependency is created every time it's requested.

  6. Scope and Component Relationship: It's important to note that a component can only reference a scope once. For instance, you can't have two @Singleton scoped components. However, different components can use the same scope annotation, but they won't share instances.

If you notice carefully, the 6th point above says that "different components can use the same scope annotation, but they won't share instances."

What this basically means is that if we create two instances of a component, both of them will have 2 different sets of singletons.

Let's try this in our own example to understand better.

We'll change our MainActivity class so that it has 2 instances of components now.

class MainActivity : AppCompatActivity() {
    var calculate : Button? = null
    var TAG = this.javaClass.canonicalName
//    var networkSetup: NetworkSetup? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        calculate = findViewById(R.id.calculate_sum)

        //Created two component instances

        val component1 = DaggerComputeComponent.builder().delay(100).status(10).networkModuleSecond(NetworkModuleSecond()).build()
        val component2 = DaggerComputeComponent.builder().delay(100).status(10).networkModuleSecond(NetworkModuleSecond()).build()

//        networkSetup = NetworkSetup()
//        computation = ComputeLayer(networkSetup)

    Log.e(TAG, "computation 1 is : ${component1.computeLayer} + and second computation is ${component2.computeLayer}" )

        calculate?.setOnClickListener {
            component1.computeLayer.add(1,1)
            Log.e(TAG, "Button click uses computeLayer to be ")
        }
    Log.e(TAG, "Main Activity Created")
    }
}

E computation 1 is : com.example.understandingdependencyinjection.ComputeLayer@c48375e + and second computation is com.example.understandingdependencyinjection.ComputeLayer@c52673f

And as you can see from the logs, we have again got 2 different objects of the same class even though we had annotated it with @singleton simply because they are part of different component scopes.

Now, let's revert our current changes and try to rotate the screen and see what happens (as you know rotating the screen, destroys and recreates an activity), we should be getting new objects on each rotation as well.

And, as you can see from the logs. We are getting 2 new objects on screen rotation as well, which is not ideal.

The Solution

For all real-life cases, we might need our object to be available for the whole application lifecycle since creating it every time we need it in our app is not optimal for heavy expensive objects like retrofit or OkHttp.

So, what we can do is create our component in the application class, and then use its instance in our app lifecycle, this way we won't need to create a component every time a new activity is created.

Let's start coding and see how this can be achieved.

Step 1

First, we need to create a class that extends the Application class, let's call it MyApp.

public class MyApp extends Application {
    private ComputeComponent component;
    @Override
    public void onCreate() {
        super.onCreate();
        // we are instantiating our component here instead of in the activity class
        component = DaggerComputeComponent.builder().delay(100).status(10).networkModuleSecond(new NetworkModuleSecond()).build();
    }

    public ComputeComponent getAppComponent(){
        //getter method for the component
        return component;
    }
}

It should be quite evident from the code above that all we have done here is instantiate our DaggerComputeComponent and then created a getter method for the object.

Step 2

Now we need to access this object in our MainActivity class

class MainActivity : AppCompatActivity() {
    var calculate : Button? = null
    var TAG = this.javaClass.canonicalName

    @Inject
    lateinit var computation : ComputeLayer

    @Inject
    lateinit var computation2 : ComputeLayer
//    var networkSetup: NetworkSetup? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        calculate = findViewById(R.id.calculate_sum)

        //Notice how the create method is chaned with a builder method instead
        //we are accessing the component here from our MyApp class
        val component = (application as MyApp).appComponent
        component.inject(this)
        //....
       }
}

Now, let's run our app and check the logs again.

And, as you can see from the logs. The hash for the object did not change even when we rotated the device, because now the component is linked with the whole app lifecycle.

Now, we can easily create our network objects in our DI modules and reuse them wherever needed in the app lifecycle without creating them again.

Conclusion

Understanding and effectively utilizing scopes in Dagger 2 is pivotal for optimizing resource management and ensuring consistent behavior across applications. By aligning components with the application lifecycle and leveraging the power of custom scopes, developers can achieve efficient and streamlined dependency management, enhancing the overall performance and user experience of their applications.