Multi-scoping Dagger Components

A Dagger Component can have more than one scope annotation. Read this article to find how this can beneficial.

April 17, 2018

Reusability is one of the many attributes that are said to contribute to a high quality code base. Dagger is in many aspects a tool which helps a lot with writing reusable code.

In this post I would like to discuss a scoping feature of Dagger 2 which is not very well known but does help a lot if you have a specific module that is reused a couple of times in the app. I am not sure if this feature has a name, but I personally call it multi-scoping of components.

The problem

Let’s say you are writing an app where a video player appears in many parts of the app. It contains a lot of different screens which all use a VideoPlayer class, which has a VideoView dependency, which is an interface defined by the video player. This class is being provided in a VideoPlayerModule and that will be included in every dagger component that needs to use the player feature. Let’s say we have a VideoActivity which uses this and a VideoFragment which can be included in other activities. Both the Activity and the fragment implement the VideoView interface.

public abstract class VideoPlayerModule {
  @Provides
  static VideoPlayer provideVideoPlayer(VideoView view) {
    return new VideoPlayer(view);
  }
}

With VideoActivity using it in a component that has ActivityScope.

@Component(modules = VideoActivityModule.class)
@ActivityScope
public interface VideoActivityComponent {
  void inject(VideoActivity activity);
}
@Module(includes = VideoPlayerModule.class)
public abstract class VideoActivityModule {
  @Binds abstract VideoView bindView(VideoActivity activity);
}

And VideoFragment using it in a component that has FragmentScope.

@Subcomponent(modules = VideoFragmentModule.class)
@FragmentScope
public interface VideoFragmentComponent {
  void inject(VideoFragment fragment);
}
@Module(includes = VideoPlayerModule.class)
public abstract class VideoFragmentModule {
  @Binds abstract VideoView provideView(VideoFragment fragment);
}

This works fine if you only inject the VideoPlayer once per component. However, it is currently un-scoped which is not what we would want for a video player.

We can not add an ActivityScope to VideoPlayerModule.provideVideoPlayer() though because it is also consumed in the fragment module. We can also not give it FragmentScope because then it can not be referenced from the ActivityPlayerComponent which is ActivityScope .

One solution might be to move the Provides methods to the activity and fragment module, but that is not sustainable when you need to scope more classes from the VideoPlayerModule.

Multi-scoping components

After some trial and error I found out that you can actually have multiple scopes on a one component. So the trick here is that you can define a scope @PlayerScope which you can use to scope any classes that are provided from the VideoPlayerModule and the you can annotate your Activity and Fragment component like this:

@Component(modules = VideoActivityModule.class)
@ActivityScope
@PlayerScope
public interface VideoActivityComponent {
  void inject(VideoActivity activity);
}
@Subcomponent(modules = VideoActivityModule.class)
@FragmentScope
@PlayerScope
public interface VideoFragmentComponent {
  void inject(VideoFragment activity);
}

Et, voila! Now you essentially have defined that within the VideoActivityComponent the lifetime of PlayerScope objects is equal to that of ActivityScope objects, and within the VideoFragmentComponent everything withPlayerScope is of equal lifetime as FragmentScope.

Note that this also works when you are using dagger-android when using ContributesAndroidInjector. You can do the following:

public abstract class ApplicationModule {
  @ContributesAndroidInjector(modules = PlayerActivityModule.class)
  @PlayerScope
  @ActivityScope
  abstract PlayerActivity contributeActivityInjector();
}

I hope you find this tip useful and till next time. and thanks Billy Chang for inspiring me to write this short post.