Hey there! Remember when we chatted about those cool dagger injection methods in our last article? We also dived into the world of modules in Dagger2. Now, we’re back at it, and this time, we’re tackling a nifty challenge: passing values to objects on the fly. No more hardcoding, folks! Let’s dive in and see how it’s done.
Consider our older example, where we added a delay of a couple of seconds to the NetworkSetup
& NetworkSetupSecond
in the constructor. Let's assume that we'd like to pass it at runtime instead of hardcoding these values.
This would obviously change the code of our NetworkSetupSecond
to this:
public class NetworkSetupSecond implements NetworkLayer{
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
public NetworkSetupSecond(StorageLayer storageLayer, int delay){
this.storageLayer = storageLayer;
try {
Log.e(TAG, "Initialising network second and delay is " + delay );
Thread.sleep(delay);
Log.e(TAG,"Network Second initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Second Created");
}
//....
}
Here, we are passing the delay as an argument in the constructor. Now we can easily pass this value to the NetworkSetupSecond
class from the NetworkModuleSecond
class where we had created this object, as shown below.
@Module
public class NetworkModuleSecond {
@Provides
NetworkLayer provideNetworkSetupSecond(StorageLayer storageLayer){
return new NetworkSetupSecond(storageLayer, 1000);
}
}
Let’s build and run this program and check the logs.
Okay, so all good so far. We are able to pass an integer to our constructor from the module.
We actually want to pass this value at runtime, which basically means that we’d somehow like to pass this value from the MainActivity to our dagger graph after the app has been created.
And here lies the problem, how would we pass the delay
from our activity to our NetworkModuleSecond
?
Fortunately, Dagger has a solution for this.
First, we need to create a constructor for our module and pass the delay
as a constructor argument as shown below.
public class NetworkModuleSecond {
int delay;
public NetworkModuleSecond(int delay){
this.delay = delay;
}
@Provides
NetworkLayer provideNetworkSetupSecond(StorageLayer storageLayer){
return new NetworkSetupSecond(storageLayer, delay);
}
}
Don’t forget to add this module to our component!
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
//annotation processing
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
}
Let’s build our project!
And we get an error!
It is basically telling us that we do not have the create method available anymore in our DaggerComputeComponent
class.
This is because the create
method is only available if none of our modules takes an argument in the constructor. The reason behind this behavior in Dagger 2 is rooted in how dependency injection works and the design principles of the Dagger framework.
When a module requires arguments in its constructor, it indicates that there’s some external dependency or configuration that Dagger cannot automatically resolve. Dagger doesn’t make assumptions about how to provide these external values. Instead, it requires the developer to explicitly provide them.
So, let’s get go ahead and provide these external values to our dagger graph from the MainActivity
.
class MainActivity : AppCompatActivity() {
var calculate : Button? = null
var TAG = this.javaClass.canonicalName
@Inject
lateinit var computation : 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().
networkModuleSecond(NetworkModuleSecond(1000)).build()
component.inject(this)
calculate?.setOnClickListener {
computation.add(1,1)
Log.e(TAG, "Button click detected")
}
Log.e(TAG, "Main Activity Created")
}
}
What we’ve done here, is that instead of letting Dagger2 create the NetworkModuleSecond
class, we created it on our own and passed the delay value as an argument.
And that’s it! We have successfully injected a value to an object in our dagger graph at runtime!
Alright, we’ve had a good run exploring how to inject values into objects at runtime with Dagger2. But guess what? We’re just scratching the surface here. Stick around for part 2 of this blog post, where we’ll dive even deeper and have some more fun with Dagger2. See you there!
Dependency injection is a powerful concept in Java, and Dagger2 has become one of the most popular frameworks for implementing it. One of the challenges developers often face is managing multiple implementations of a particular dependency. This article delves into how to provide interfaces in Java using Dagger2 principles to address this challenge.
Imagine a scenario where an application has two different types of NetworkSetup
classes (NetworkSetup
& NetworkSetupSecond
). The app needs to decide which version of the dependency to create and inject at different times. If we were to inject a new NetworkSetupSecond
class into our other classes (ComputeLayer
), we'd have to refactor all the code, replacing all references of the first class with the second. This approach is not scalable and makes swapping classes cumbersome.
Interfaces act as a contract or promise. When a class implements an interface, it agrees to provide specific behaviors (methods) listed in the interface. This allows different classes to be treated similarly based on the methods they’ve agreed to implement.
To address our problem, we can:
NetworkLayer
that lists the standard methods we plan to use in both NetworkSetup
class types.public interface NetworkLayer {
boolean sendDataToCloud(String data);
boolean saveDataToStorage(String data);
}
NetworkSetup
and NetworkSetupSecond
classes will implement the NetworkLayer
interface. This ensures that both classes adhere to the contract defined by the interface.public class NetworkSetupSecond implements NetworkLayer{
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
@Inject
public NetworkSetupSecond(StorageLayer storageLayer){
this.storageLayer = storageLayer;
try {
Log.e(TAG, "Initialising network second" );
Thread.sleep(3000);
Log.e(TAG,"Network Second initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Second Created");
}
@Override
public boolean sendDataToCloud(String data) {
Log.e(TAG,"Sending Data to cloud from second :: " + data);
return true;
}
@Override
public boolean saveDataToStorage(String data) {
storageLayer.saveDataToStorage(data);
return true;
}
}
public class NetworkSetup implements NetworkLayer {
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
@Inject
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");
}
@Override
public boolean sendDataToCloud(String data) {
Log.e(TAG,"Sending Data to cloud :: " + data);
return true;
}
@Override
public boolean saveDataToStorage(String data) {
storageLayer.saveDataToStorage(data);
return true;
}
}
NetworkLayer
interface. This provides flexibility, allowing any class that implements the NetworkLayer
interface to be injected.public class ComputeLayer {
String TAG = this.getClass().getCanonicalName();
NetworkLayer network;
@Inject
ComputeLayer(NetworkLayer networkLayer){
this.network = networkLayer;
Log.e(TAG, "Compute Layer Created");
}
//...
}
This approach allows us to create any number of variations for the NetworkLayer
class, as long as they implement the NetworkLayer
interface.
While this approach solves the problem of flexibility, Dagger2 will throw an error when trying to inject an interface. This is because Dagger2 doesn’t know how to instantiate an interface.
To address this, we can use Dagger2’s @Module
and @Provides
annotations:
NetworkLayer
interface to Dagger2.@Module
public class NetworkModule {
@Provides
NetworkLayer provideNetworkSetup(StorageLayer storageLayer){
return new NetworkSetup(storageLayer);
}
@Provides
NetworkLayer provideNetworkSetupSecond(StorageLayer storageLayer){
return new NetworkSetupSecond(storageLayer);
}
}
@Component (modules = NetworkModule.class)
public interface ComputeComponent {
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
}
However, Dagger2 will throw an error if multiple provider methods are available for the same interface. To resolve this, we can comment out one of the provider methods, depending on which implementation we want to inject.
Building the project above, throws an error again, this time it says that NetworkLayer
is bound multiple times. This error should be understandable because we have instantiated two different objects of the same superclass type NetworkLayer
using the provideNetworkSetup
& provideNetworkSetupSecond
provider method. Now dagger doesn't know how to decide which object should be injected when!
The solution to this problem is also pretty straightforward, all we need to do is comment out one of the provider methods, and dagger will automatically provide the other class object.
Let’s try to comment out the provideNetworkSetupSecond
method, run the app and check the logs.
And sure enough, we have our NetworkLayer class instantiated.
Now let’s try to comment out provideNetworkSetup
and uncomment the provideNetworkSetupSecond
method to check the logs again.
And just like that, we’ve now implemented a different NetworkLayer version to our project by changing just a couple of lines of code.
If you are following this blog post till now, you might have guessed that there is one more way to optimize all of this implementation even more, by creating different modules for different NetworkLayer types.
Let’s see how we can do this.
First of all, we need to create a second NetworkLayer
Module, let's call it NetworkLayerSecond
@Module
public class NetworkModuleSecond {
@Provides
NetworkLayer provideNetworkSetupSecond(StorageLayer storageLayer){
return new NetworkSetupSecond(storageLayer);
}
}
Now instead of having 2 provider methods in the same class, we’ll have one provider for one NetworkLayer class type in one module.
This way, we can simply swap the modules in our component class without even commenting out any code, like this.
@Component (modules = NetworkModule.class)
// you can replace NetworkModule with NetworkModuleSecond
// and it'll simply swap the underlying implementation for you.
public interface ComputeComponent {
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
}
Please note, that we cannot provide both the NetworkModule
and NetworkModuleSecond
at the same time, because Dagger would again not be able to decide which object to inject in the dagger graph.
And that’s it, now you have successfully injected an Interface implementation into your project.
Using interfaces in conjunction with Dagger2 provides a flexible and scalable approach to dependency injection in Java. By defining a contract with interfaces and leveraging Dagger2’s module and provider mechanisms, developers can easily manage and swap multiple implementations of a particular dependency.
This exploration underscores the versatility and depth of Dagger2 as a dependency injection framework. It also highlights the importance of continuous learning and adaptation in the ever-evolving world of software development.
As we wrap up this segment, rest assured that our journey with Dagger2 is far from over. In upcoming discussions, we will delve into even more advanced topics within dependency injection, ensuring that you remain at the forefront of best practices and innovative solutions. Stay tuned for more insights and discoveries on Dagger2 and beyond.
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:
@Inject
and Dagger2, like a seasoned sorcerer, instinctively knew how to conjure instances of the class. Ah, those were simpler times!But today, we venture into uncharted territories.
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 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.
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.
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.
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.
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!
@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
. 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.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.
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.