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:
Same Object Requirement: We need the same object instance throughout the Dagger graph.
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.