Multibinding with Dagger2: (Day 15)

Photo by Dan Bucko on Unsplash

Multibinding with Dagger2: (Day 15)

Play this article

Multibinding is a powerful feature in Dagger 2 that allows you to bind multiple objects into a collection, like a set or a map, even when the objects and the collections are bound in different modules.

But before we dive deeper into this topic, we first need to understand the need for multibinding. And to do that, let's rollback a few blog posts, when we were discussing Dependency Injection (DI) in Android: Providing Interfaces (Day6)

You might remember from that post that we faced an issue when we tried to instantiate both NetworkModule & NetworkModuleSecond at the same time.

This error was understandable because we did not had a way of telling Dagger2, which version of the NetworkLayer should be used for our project.

By the way, if you want to replicate the issue, just add both the NetworkModule and the NetworkModuleSecond to the ComputeComponent as shown below.

@Singleton
@Component (modules = {
                        AppModule.class
                        , AndroidSupportInjectionModule.class
                        , NetworkModuleSecond.class
                        , NetworkModule.class
                        })
public interface ComputeComponent extends AndroidInjector<MyApp> {

    @Component.Factory
    interface Factory {
        ComputeComponent create(@BindsInstance @Named("delay") int delay
                , @BindsInstance @Named("status")int status,
                                NetworkModuleSecond networkModuleSecond, NetworkModule networkModule);
    }
}

It's pretty obvious that as the project size increases, handling different modules and different implement versions of the same Superclass could get difficult. This is where multibinding comes into play.

1. Set Multibindings:

With Set multibindings, you can accumulate multiple bindings into a Set.

How to use:

  1. Declare the Return Type as Set<T>: In your module, declare a method that returns Set<T>.

  2. Use @IntoSet Annotation: For each binding you want to add to the set, annotate the @Provides method with @IntoSet.

@Module
public abstract class MyModule {
    @Provides
    @IntoSet
    static String provideOneString() {
        return "One";
    }

    @Provides
    @IntoSet
    static String provideAnotherString() {
        return "Another";
    }
}

When you request a Set<String> In your component or dependent objects, Dagger will provide a set containing both "One" and "Another".

Remember

IntoSet returns all the objects that are annotated with @IntoSet as a single set, this kind of implementation could be helpful in cases like that of an Event Handling System.

For example, suppose you have a centralized event dispatcher class that registers all the events and then notifies registered handlers. Now we wouldn't want to change the event dispatcher class for each new event handler getting added to the code, in such a case @IntoSet could get in handy, as we can just annotate our new implementation of the event with the same and it'll automatically be provided with the rest of the events to the centralized event dispatcher.

2. Map Multibindings:

Map multibindings allow you to accumulate multiple bindings into a Map where each binding is associated with a key.

This is something that we'll implement into our own project and see how it makes our code more modular and maintainable.

Step 1

First, we need to annotate our NetworkModule provider method and the NetworkModuleSecond provider method with @IntoMap as shown below.

@Module
public class NetworkModuleSecond {
    @Singleton
    @Provides
    @IntoMap
    @StringKey("provideNetworkSetupSecond")
    NetworkLayer provideNetworkSetupSecond(NetworkSetupSecond networkSetupSecond){
        return networkSetupSecond;
    }
}
@Module
public class NetworkModule {
    @Singleton
    @Provides
    @IntoMap
    @StringKey("provideNetworkSetup")
    NetworkLayer provideNetworkSetup(NetworkSetup networkSetup){
        return networkSetup;
    }
}

Remember

The @StringKey will be used as an identifier, hence it should be unique for each module.

Step 2

Next, we need to change the injected object in our ComputeLayer class as follows.

@PerActivity
public class ComputeLayer {
    String TAG = this.getClass().getCanonicalName();
    String layer = "Computation";
    Map<String, NetworkLayer> network;

    @Inject
    public ComputeLayer(Map<String, NetworkLayer> networkLayer){
        this.network = networkLayer;
        Log.e(TAG, "Compute Layer Created uses network layer as " + networkLayer);
    }

    public boolean add (int a , int b){
        int c = a+b;
        System.out.println(layer + " addition result " + c );
        return Objects.requireNonNull(network.get("provideNetworkSetup")).sendDataToCloud(Integer.toString(c));
    }


    public void subtract (int a , int b){
        int c = a-b;
        Log.e(TAG, " subtraction result " + c );
        Objects.requireNonNull(network.get("provideNetworkSetupSecond")).sendDataToCloud(Integer.toString(c));
    }
//...
}

The newly created hashmap network will from now on have all the versions of the NetworkLayer implementation that is marked with @IntoMap. Also, notice that while the add function is using "provideNetworkSetup", the subtract function is using "provideNetworkSetupSecond". This will later help us understand how we can use two different implementations of NetworkLayer in the same ComputeLayer as well.

Step 3

Now we'll update our MyApp class and also provide it with an instance of NetworkModule as shown below.

public class MyApp extends DaggerApplication {
    private ComputeComponent component;
    @Override
    public void onCreate() {
        super.onCreate();

        component = DaggerComputeComponent.factory().create(3000, 10, new NetworkModuleSecond(), new NetworkModule());
    }

    public ComputeComponent getAppComponent(){
        return component;
    }

    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
        return DaggerComputeComponent.factory().create(1000, 10, new NetworkModuleSecond(), new NetworkModule());
    }
}

For testing, I've added a new button subtract and called the subtract method of ComputeLayer as follows.

 subtract?.setOnClickListener {
            Log.e(TAG, "Uses ComputeLayer instance with hash ${lazyComputation.get()}")
            lazyComputation.get()?.subtract(1,1)
        }

Now, let's build the project and run to see the result!

The first log is from the addition button click, while the second log is from the subtract button click. And as you can see, both of them come from two different classes i.e NetworkModuleSecond & NetworkModule.

Also, an interesting thing to note is that Dagger is now instantiating both NetworkLayer implementations and simply using one of the two for different use cases. This is particularly helpful in cases where we have different flavors of the same implementation and we want to use them interchangeably.

Also

  • @StringKey is not the only way to create unique identifiers for our classes, there are other annotations as well like @Classkey which helps us identify the classes from class names directly (you can also create your custom annotations here), we'll look into this in much detail when we try to inject ViewModels in our future blog posts.

  • We can also create an empty map and then manually add the desired class objects to it using an annotation @Multibinds.

Use Cases and Benefits:

  1. Dynamic Injection: Multibindings are useful when you have a dynamic list of objects or tasks that you want to inject. For instance, a list of all available network request interceptors or a set of task runners.

  2. Decoupling: Different modules can contribute to the same collection without having a direct dependency on each other. This promotes a decoupled architecture.

  3. Extensibility: It's easy to add new bindings to the collection without modifying existing code. This is especially useful in large projects or libraries where you want to allow extensions or plugins.

  4. Key-Value Pair Management: With map multibindings, managing key-value pairs becomes straightforward. This is useful in scenarios where you need to provide different implementations based on a key, like different feature flags or strategies.

Conclusion:

Multibinding in Dagger 2 provides a way to collect multiple bindings into cohesive sets or maps, making it easier to manage and inject collections of related objects. It's a powerful tool that can simplify dependency management and make your DI setup more flexible and extensible.