Photo by Daniel Romero on Unsplash
From Constructors to Third-Party Libraries: DI (Day 5)
A Deep Dive into @Module & @Provides
In our previous explorations, we embarked on a journey through the intricate world of Dagger2, diving deep into the mechanisms of dependency injection. We discovered three primary techniques to weave this magic:
Constructor Injection: Remember when we annotated constructors with
@Inject
and Dagger2, like a seasoned sorcerer, instinctively knew how to conjure instances of the class. Ah, those were simpler times!Field Injection: Then came the challenges of the Android realm, where Activities and Fragments, those elusive creatures, were birthed by Android itself. Without control over their creation, we turned to field injection, our trusty ally.
Method Injection: On the rare occasion, when an object needed a nudge after its birth, method injection was our subtle charm.
But today, we venture into uncharted territories.
Imagine!
Imagine a third-party library (retrofit is a good example), since the code of this library would be locked, we cannot modify its constructor, meaning we cannot use constructor injection for this library.
We learned earlier that field injection is done when we do not have access to the constructor of a class, so can we use that here?
Well, it might look like a good solution but there’s something very important here that we seem to be forgetting!
Field injection is about where and how the dependencies are injected i.e. it helps us inject a dependency into a class that doesn't have a constructor to access (typically), like Activity, Fragments, etc.
But in our above case scenario, we seem to not have the access to the constructor of the dependency itself!
As Dagger relies on having access to constructors to instantiate dependencies. If a class's constructor is not accessible (e.g., for third-party libraries or for classes without a public constructor), Dagger cannot directly instantiate it.
But!
Read the last line carefully, and notice that dagger2 cannot “instantiate” the class, BUT it should be able to provide it to our dagger graph whenever needed IF we can somehow create this object ourselves and provide it to Dagger.
This is where @Module
& @Provides
annotations come into play. The @Module
class contains methods annotated with @Provides
. These methods help us create and configure instances of dependencies. This is especially useful for cases where you need to provide custom configurations or when dealing with third-party libraries.
Okay! Let's Code
Now that we have understood the need for these methods, let us try to understand how we can actually use them.
So first of all, let us create a class named StorageLayer
. We’ll create a simple builder method in this class, whose task will only be to update a string. This way, we’ll know when the builder method has been successfully called.
public class StorageLayer {
String TAG = this.getClass().getCanonicalName();
public String builderString = "Build Status:: Not yet done";
public StorageLayer(){
try {
Log.e(TAG, "Initialising Storage" );
Thread.sleep(1000);
Log.e(TAG,"Storage initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Storage Layer Created");
}
public boolean saveDataToStorage(String data){
Log.e(TAG,"Saving data to storage:: " + data + "builder " + builderString);
return true;
}
public void builder(){
builderString = "Build Status:: Done";
}
}
Notice that this is just a simple class without any dependency injection principles, we are imagining this class to be a third-party library.
Now let’s update our NetworkSetup
class.
This class will depend on StorageLayer
class, BUT we are imagining this class to also be a third-party library so we need to remove the @inject
annotation from the constructor.
public class NetworkSetup {
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
public NetworkSetup(StorageLayer storageLayer){
this.storageLayer = storageLayer;
try {
Log.e(TAG, "Initialising network" );
Thread.sleep(6000);
Log.e(TAG,"Network initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Created");
}
public boolean sendDataToCloud(String data){
Log.e(TAG,"Sending Data to cloud :: " + data);
saveDataToStorage(data);
return true;
}
public boolean saveDataToStorage(String data){
storageLayer.saveDataToStorage(data);
return true;
}
}
So, no dependency injection principles are implemented here as well.
Let's try to build our project and see what happens.
Compilation Error!
The error message is simply a warning about us removing the @inject
annotation from the constructor, because now Dagger has no idea how to provide it.
So let’s go ahead and use @Module
and @Provides
annotation to see how we can help Dagger provide our StorageLayer
& NetworkSetup
class.
Network Module
Let's start by creating a simple NetworkModule class and annotating it with @Module
like this:
@Module
public class NetworkModule {
}
Next, we need to create provider methods to help Dagger understand how to inject the StorageLayer
& NetworkSetup
objects.
@Module
public class NetworkModule {
@Provides
StorageLayer provideStorageLayer(){
StorageLayer storageLayer = new StorageLayer();
return storageLayer;
}
@Provides
NetworkSetup provideNetworkSetup(StorageLayer storageLayer){
return new NetworkSetup(storageLayer);
}
}
Whenever writing these provider methods, remember that the name of the method doesn't matters, only its return type.
Also, notice that NetworkSetup
class needs an instance of the StorageLayer
class, but this is not something that we’ll provide, we just have to create the object and hand it over to Dagger, the decision of injecting it wherever needed is done by Dagger’s Acyclic Graph itself.
Now let’s head over to the ComputeComponent
class and add this module to our project. To do this, we just need to update the @Component
annotation with @Component (modules = NetworkModule.class)
And that's it, let's run the app and check the logs.
Just as we'd expect, we have all the dependencies working!
Let’s now try to click the Calculate button and see what message we see in the logs.
It says, that Build Status is "Not yet done", which is just the default string that we set for our builderString
variable.
This is just a simple example where we didn't need any custom configuration on our object before configuration. But what if this StorageLayer needed a custom file path where it should store the files or a variable to decide if the location should be set for production or debug testing?
In that case, we'd need to configure the object before we inject it into our dagger graph, which is precisely what we'll demonstrate next:
So let's go ahead and call the builder function before we inject it into the graph.
@Module
public class NetworkModule {
@Provides
StorageLayer provideStorageLayer(){
StorageLayer storageLayer = new StorageLayer();
storageLayer.builder(); //builder called before injection
return storageLayer;
}
@Provides
NetworkSetup provideNetworkSetup(StorageLayer storageLayer){
return new NetworkSetup(storageLayer);
}
}
Now that we've called the builder method before injecting our object, let's check the logs again on the button click.
And just as expected, now the build status shows done!
A Quick Summary
@Module
is an annotation used on a class. This class defines methods that provide dependencies. Think of a module as a factory that creates objects. The reason we need modules is that not all objects can be created by simply invoking a constructor. Some objects require complex initialization, or they come from third-party libraries, or you might want to mock an object during testing.
Here's a simple breakdown:
Provides Dependencies: Methods inside a module are annotated with
@Provides
. These methods define how to provide a dependency. For instance, if you have a third-party library object or an object that requires complex initialization, you'd define a method in a module to provide that object.Reusable and Testable: By defining object creation in modules, you can easily swap modules during testing. This makes mocking and testing easier.
Custom Configuration: Sometimes, you might want to provide a dependency in a specific way based on some conditions. Modules allow for this custom configuration.
In summary, while injections (constructor, field, method) tell Dagger where to provide the dependencies, @Module
tells Dagger how to provide those dependencies. They work hand in hand to ensure that your objects are constructed and injected correctly.
Conclusion
As we conclude this segment of our Dagger2 series, it's clear that the world of dependency injection is vast and filled with nuances. We've delved into the foundational aspects, from the elegance of constructor injection to the intricacies of integrating third-party libraries. But this is just the beginning. The road ahead promises even more insights as we explore how to provide interfaces and tackle advanced Dagger2 topics. Stay tuned, for our journey into the heart of Dagger2 is far from over. Together, we'll continue to unravel its complexities and harness its full potential in our Android applications.