Dagger 2 Annotations Explained: @COMPONENT.BUILDER - @NAMED (Day 08)

Photo by Louis Tsai on Unsplash

Dagger 2 Annotations Explained: @COMPONENT.BUILDER - @NAMED (Day 08)

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!