In our last blog post, we explored how we can inject values into our dagger graph at runtime by passing them to a module using its constructor. Today we’ll see how instead of passing our value to the module and module to the builder, we can also pass these values to the builder directly.
First of all, let’s go ahead and add the @inject annotation back to our NetworkSetupSecond
class, and pass the delay as a constructor parameter as shown below.
public class NetworkSetupSecond implements NetworkLayer{
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
@Inject
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");
}
//...
}
Next, we'll update our NetworkModuleSecond
class and remove the constructor. as we won't need it anymore.
@Module
public class NetworkModuleSecond {
@Provides
NetworkLayer provideNetworkSetupSecond(NetworkSetupSecond networkSetupSecond){
return networkSetupSecond;
}
}
Now comes the interesting part!
Let's update our ComputeComponent
and add our custom builder methods to provide the delay
value to our module.
To do this, we'll be using @Component.Builder
and @BindsInstance
annotations, so let's start coding!
Step 1:
First of all, we need to add a nested interface inside our ComputeComponent
interface as shown below
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
//annotation processing
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
@Component.Builder
interface Builder{
}
}
The @COMPONENT.BUILDER
annotation is used to define the API for Dagger's component builder. By creating a nested interface within the component and annotating it with @COMPONENT.BUILDER
, we can specify the methods available on the builder. This is particularly useful when you want to define your own methods on the builder, rather than relying on the auto-generated ones.
Also, keep in mind that it's mandatory for us to override all the methods that were present earlier, so for e.g when we go to our MainActivity
class and check the override methods available under the builder we see 2 such methods which are
networkModuleSecond
& build()
Step 2:
So, our code would now look like this.
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
//annotation processing
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
@Component.Builder
interface Builder{
ComputeComponent build();
Builder networkModuleSecond(NetworkModuleSecond networkModuleSecond);
}
}
Step 3:
Now we'll add our delay method in the builder as shown below.
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
//annotation processing
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
@Component.Builder
interface Builder{
ComputeComponent build();
//HERE WE ADDED THE DELAY METHOD
@BindsInstance
Builder delay(int delay);
Builder networkModuleSecond(NetworkModuleSecond networkModuleSecond);
}
}
The @BINDSINSTANCE
annotation offers a more efficient way to get variables into the dependency graph at runtime. Instead of passing values to a module and then adding that module to the component, you can directly pass the values to the builder.
An important point to remember here is that Dagger only cares about the object type and not the variable name. So in our above delay method, what we are providing is an int
to our dagger graph, the name of the variable doesn't matters.
Step 4:
Let's go ahead and call this method in MainActivity
as shown below.
val component = DaggerComputeComponent.builder().delay(100).networkModuleSecond(NetworkModuleSecond()).build()
component.inject(this)
And that should be it!
If we build and run our app now, we should be able to see our delay in the logs.
But Wait!
Let's see what would happen if we add another int
variable (status
) to our NetworkSetupSecond
class.
public class NetworkSetupSecond implements NetworkLayer{
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
@Inject
public NetworkSetupSecond(StorageLayer storageLayer, int delay, int status){
this.storageLayer = storageLayer;
try {
Log.e(TAG, "Initialising network second and delay is :: " + delay + " and status is :: " + status );
Thread.sleep(delay);
Log.e(TAG,"Network Second initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Second Created");
}
//...
}
Let's run the code and check our logs:
While the code compiles and runs successfully, one important thing to notice is that we are getting the value of status
variable also as 100, which is obviously not correct.
Why did this happen?
Well if you remember, we had discussed a couple of paragraphs above that Dagger2 only cares about the data type of a variable while providing it to the dagger graph, it doesn't matter what we name the variable, that's exactly what happened here.
Since status
is also an int
datatype, Dagger2 decided to provide it the value that we injected for delay
automatically.
This is obviously a problem since we might like to inject multiple values to variables in the future. So how do we do it?
Enters @NAMED
annotation
When dealing with multiple bindings of the same type, Dagger can face ambiguity. For instance, if you have two different integer values that need to be injected, Dagger wouldn't know which one to use where. This is where the @NAMED
annotation comes in handy.
By annotating a variable with @NAMED
and providing a string identifier, you can distinguish between multiple bindings of the same type.
Let's implement this concept into our code.
First, we'll update our NetworkSetupSecond class.
public class NetworkSetupSecond implements NetworkLayer{
String TAG = this.getClass().getCanonicalName();
StorageLayer storageLayer;
@Inject
public NetworkSetupSecond(StorageLayer storageLayer, @Named("delay") int delay, @Named("status") int status){
this.storageLayer = storageLayer;
try {
Log.e(TAG, "Initialising network second and delay is :: " + delay + " and status is :: " + status );
Thread.sleep(delay);
Log.e(TAG,"Network Second initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Second Created");
}
}
Notice that we have annotated the arguments in the constructor with arbitrary strings, that we'll use later to identify our variables.
Let's go ahead and update our ComputeComponent
class next.
@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);
}
}
We have used the exact same string here to let Dagger2 know which int datatype should be used for which argument.
That's it, now all we need to do is to call this method from our MainActivity as shown below.
val component = DaggerComputeComponent.builder()
.delay(100)
.status(10)
.networkModuleSecond(NetworkModuleSecond())
.build()
component.inject(this)
And that's it! If we build and run our project now, we should see 2 different values for both our variables.
Conclusion
Navigating Dagger 2 can initially seem complex but with annotations like @COMPONENT.BUILDER
, @BINDSINSTANCE
, and @NAMED
, it becomes a more manageable journey. These tools are essential for enhancing code efficiency and clarity in dependency management. And remember, this is just the beginning. In our upcoming blog posts, we'll delve deeper into other annotations such as @Singleton
and explore the world of custom scopes. Stay engaged, and we'll continue this enlightening journey together. Happy coding!