Scoping & Singletons: DI (Day - 09)

Photo by Sigmund on Unsplash

Scoping & Singletons: DI (Day - 09)

Dependency injection is a powerful technique that allows for better modularity and testing in applications. Dagger2, a popular dependency injection framework, provides various scopes to manage the lifecycle of objects. One of the most commonly used scopes is the Singleton scope. This article delves deep into the Singleton scope in Dagger2, its significance, and its implementation.

The Scenario

In our journey with Dagger2, we've been creating a single object for each class and injecting it into our Dagger graph. However, there are situations where we might need multiple instances of a class. In such scenarios, two primary cases can arise:

  1. Same Object Requirement: We need the same object instance throughout the Dagger graph.

  2. New Object Requirement: We require a new instantiated object at specific places every time.

Let's see what happens if we try to inject a new ComputeLayer object into our MainActivity right now.

For this, we'll go into our MainActivity class and inject another ComputeLayer object as shown below.

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

        val component = DaggerComputeComponent.builder().delay(100).status(10).networkModuleSecond(NetworkModuleSecond()).build()
        component.inject(this)

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

    Log.e(TAG, "computation 1 is : $computation + and second computation is $computation2" )

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

Notice that under the onCreate method, we have added a new log to check the hashes of the generated object, since these hashes are always unique for objects, we'll be able to tell when new objects are created.

So let's go ahead and run this app to see what we get in the logs.

E computation 1 is : com.example.understandingdependencyinjection.ComputeLayer@785643f + and second computation is com.example.understandingdependencyinjection.ComputeLayer@11110c

As we expected, you can see that the hashes are different for both objects (785643f & 11110c ).

But this raises further questions, like does it means that all the objects that the ComputeLayer needs, will also be initialized every time?

To verify this, let's put a log under the constructor of the ComputeLayer class and track how many times the NetworkLayer class was created

public class ComputeLayer {
    String TAG = this.getClass().getCanonicalName();
    String layer = "Computation";
    NetworkLayer network;

    @Inject
    ComputeLayer(NetworkLayer networkLayer){
        this.network = networkLayer;
        Log.e(TAG, "Compute Layer Created uses network layer as " + networkLayer);
    }
//...
}

Let's run the app and recheck the logs.

And as you can see, there are 2 different NetworkSetup objects also created.

But, don't you think this is unnecessary?

We should be able to use the same NetworkSetup object across all our app since we'll be needing network calls everywhere, and initializing it every time might not be the most optimal solution.

So now the question arises, how can we tell Dagger that it should use the same object at all places?

Enter Scoping and Singletons

To address the above challenge, Dagger2 introduced the concept of scoping and singletons. By annotating an object with @Singleton, Dagger2 ensures that the object is created only once within its scope and is reused whenever needed.

So let's go ahead and see how we can implement it!

Step 1:

First of all, we need to annotate our NetworkLayer class with @Singleton annotation, but remember that we never annotate the Interface itself, but we annotate the component or the module's method that provides the implementation for that interface with @Singleton.

So in our case, it would be like this

@Module
public class NetworkModuleSecond {
    @Singleton
    @Provides
    NetworkLayer provideNetworkSetupSecond(NetworkSetupSecond networkSetupSecond){
        return networkSetupSecond;
    }
}

We updated the NetworkSetupSecond provider method as shown above with the singleton annotation.

Step 2:

Annotate the component that references the module with @Singleton. This ensures that the component and all its dependencies adhere to the Singleton scope:

@Singleton
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
    //annotation processing

    ComputeLayer getComputeLayer();

    void inject  (MainActivity mainActivity);
    @Component.Builder
    interface Builder{
        ComputeComponent build();

        @BindsInstance
        Builder delay(@Named("delay") int delay);

        @BindsInstance
        Builder status(@Named("status")int status);

        Builder networkModuleSecond(NetworkModuleSecond networkModuleSecond);
    }
}

And that's it, if we build and run our project now, we should get the same NetworkLayer object even though the ComputeLayer object would be different.

Result

E computation 1 is : com.example.understandingdependencyinjection.ComputeLayer@a49c6c5 + and second computation is com.example.understandingdependencyinjection.ComputeLayer@d57d81a

And, just as expected. The ComputeLayer objects are different i.e a49c6c5 & d57d81a, but the NetworkSetup object is the same i.e f66e93c.

Now, let's try making our ComputeLayer singleton as well, for this, we'll annotate our ComputeLayer class itself with the singleton annotation as shown below.

@Singleton
public class ComputeLayer {
    String TAG = this.getClass().getCanonicalName();
    String layer = "Computation";
    NetworkLayer network;

    @Inject
    ComputeLayer(NetworkLayer networkLayer){
        this.network = networkLayer;
        Log.e(TAG, "Compute Layer Created uses network layer as " + networkLayer);
    }
//...
}

And that should do the trick!

Let's run our app again and check the logs.

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

And as you can see in the logs above, we have the same object for ComputeLayer as well i.e a49c6c5.

Wrapping Up

The Singleton scope in Dagger2 is a testament to the framework's flexibility and power. By understanding and leveraging this scope, developers can optimize their applications, ensuring efficient memory usage and consistent behavior across different parts of their Dagger graph. Whether it's reusing network configurations or ensuring consistent data layers, the Singleton scope in Dagger2 is an indispensable tool in a developer's arsenal.