Activities Subcomponents Multibinding in Dagger 2
A couple months ago, during MCE³ conference, Gregory Kick in his presentation showed a new concept of providing Subcomponents (e.g. to Activities). New approach should give us a way to create ActivitySubcomponent without having AppComponent object reference (which used to be a factory for Activities Subcomponents). To make it real we had to wait for a new release of Dagger: version 2.7.
The problem
Before Dagger 2.7, to create Subcomponent (e.g. MainActivityComponent
which is Subcomponent of AppComponent
) we had to declare its factory in parent Component:
@Singleton | |
@Component( | |
modules = { | |
AppModule.class | |
} | |
) | |
public interface AppComponent { | |
MainActivityComponent plus(MainActivityComponent.ModuleImpl module); | |
//... | |
} |
Thanks to this declaration Dagger knows that MainActivityComponent
has access to dependencies from AppComponent
.
Having this, injection in MainActivity
looks similar to:
@Override | |
protected ActivityComponent onCreateComponent() { | |
((MyApplication) getApplication()).getComponent().plus(new MainActivityComponent.ModuleImpl(this)); | |
component.inject(this); | |
return component; | |
} |
The problems with this code are:
-
Activity depends on
AppComponent
(returned by((MyApplication) getApplication()).getComponent())
— whenever we want to create Subcomponent, we need to have access to parent Component object. -
AppComponent
has to have declared factories for all Subcomponents (or their builders), e.g.:MainActivityComponent plus(MainActivityComponent.ModuleImpl module);
.
Modules.subcomponents
Starting from Dagger 2.7 we have new way to declare parents of Subcomponents. @Module
annotation has optional subcomponents field which gets list of Subcomponents classes, which should be children of the Component in which this module is installed.
Example:
@Module( | |
subcomponents = { | |
MainActivityComponent.class, | |
SecondActivityComponent.class | |
}) | |
public abstract class ActivityBindingModule { | |
//... | |
} |
ActivityBindingModule
is installed in AppComponent
. It means that both: MainActivityComponent
and SecondActivityComponent
are Subcomponents of AppComponent
.
Subcomponents declared in this way don’t have to be declared explicitly in AppComponent
(like was done in first code listing in this post).
Activities Multibinding
Let’s see how we could use Modules.subcomponents
to build Activities Multibinding and get rid of AppComponent object passed to Activity (it’s also explained at the end of this presentation). I’ll go only through the most important pieces in code.
Whole implementation is available on Github: Dagger2Recipes-ActivitiesMultibinding.
Our app contains two simple screens: MainActivity
and SecondActivity
. We want to be able to provide Subcomponents to both of them without passing AppComponent
object.
Let’s start from building a base interface for all Activity Components builders:
public interface ActivityComponentBuilder<M extends ActivityModule, C extends ActivityComponent> { | |
ActivityComponentBuilder<M, C> activityModule(M activityModule); | |
C build(); | |
} |
Example Subcomponent: MainActivityComponent
could look like this:
@ActivityScope | |
@Subcomponent( | |
modules = MainActivityComponent.MainActivityModule.class | |
) | |
public interface MainActivityComponent extends ActivityComponent<MainActivity> { | |
@Subcomponent.Builder | |
interface Builder extends ActivityComponentBuilder<MainActivityModule, MainActivityComponent> { | |
} | |
@Module | |
class MainActivityModule extends ActivityModule<MainActivity> { | |
MainActivityModule(MainActivity activity) { | |
super(activity); | |
} | |
} | |
} |
Now we would like to have Map
of Subcomponents builders to be able to get intended builder for each Activity class. Let’s use Multibinding
feature for this:
@Module( | |
subcomponents = { | |
MainActivityComponent.class, | |
SecondActivityComponent.class | |
}) | |
public abstract class ActivityBindingModule { | |
@Binds | |
@IntoMap | |
@ActivityKey(MainActivity.class) | |
public abstract ActivityComponentBuilder mainActivityComponentBuilder(MainActivityComponent.Builder impl); | |
@Binds | |
@IntoMap | |
@ActivityKey(SecondActivity.class) | |
public abstract ActivityComponentBuilder secondActivityComponentBuilder(SecondActivityComponent.Builder impl); | |
} |
ActivityBindingModule
is installed in AppComponent
. Like it was explained, thanks to this MainActivityComponent
and SecondActivityComponent
will be Subcomponent of AppComponent
.
Now we can inject Map of Subcomponents
builder (e.g. to MyApplication
class):
public class MyApplication extends Application implements HasActivitySubcomponentBuilders { | |
@Inject | |
Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders; | |
private AppComponent appComponent; | |
public static HasActivitySubcomponentBuilders get(Context context) { | |
return ((HasActivitySubcomponentBuilders) context.getApplicationContext()); | |
} | |
@Override | |
public void onCreate() { | |
super.onCreate(); | |
appComponent = DaggerAppComponent.create(); | |
appComponent.inject(this); | |
} | |
@Override | |
public ActivityComponentBuilder getActivityComponentBuilder(Class<? extends Activity> activityClass) { | |
return activityComponentBuilders.get(activityClass); | |
} | |
} |
To have additional abstraction we created HasActivitySubcomponentBuilders
interface (because Map
of builders doesn’t have to be injected into Application
class):
public interface HasActivitySubcomponentBuilders { | |
ActivityComponentBuilder getActivityComponentBuilder(Class<? extends Activity> activityClass); | |
} |
And the final implementation of injection in Activity class:
public class MainActivity extends BaseActivity { | |
//... | |
@Override | |
protected void injectMembers(HasActivitySubcomponentBuilders hasActivitySubcomponentBuilders) { | |
((MainActivityComponent.Builder) hasActivitySubcomponentBuilders.getActivityComponentBuilder(MainActivity.class)) | |
.activityModule(new MainActivityComponent.MainActivityModule(this)) | |
.build().injectMembers(this); | |
} | |
} |
It’s pretty similar to our very first implementation, but as mentioned, the most important thing is that we don’t pass ActivityComponent
object to our Activities anymore.
Example of use case — instrumentation tests mocking
Besides loose coupling and fixed circular dependency (Activity <-> Application) which not always is a big issue, especially in smaller projects/teams, let’s consider the real use case where our implementation could be helpful — mocking dependencies in instrumentation testing.
Currently one of the most known way of mocking dependencies in Android Instrumentation Tests is by using DaggerMock (Github project link). While DaggerMock is powerful tool, it’s pretty hard to understand how it works under the hood. Among the others there is some reflection code which isn’t easy to trace.
Building Subcomponent directly in Activity, without accessing AppComponent class gives us a way to test every single Activity decoupled from the rest of our app.
Sounds cool, now take a look at code.
Application class used in our instrumentation tests:
public class ApplicationMock extends MyApplication { | |
public void putActivityComponentBuilder(ActivityComponentBuilder builder, Class<? extends Activity> cls) { | |
Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders = new HashMap<>(this.activityComponentBuilders); | |
activityComponentBuilders.put(cls, builder); | |
this.activityComponentBuilders = activityComponentBuilders; | |
} | |
} |
Method putActivityComponentBuilder()
gives us a way to replace implementation of ActivityComponentBuilder for given Activity class.
Now take a look at our example Espresso Instrumentation Test:
@RunWith(AndroidJUnit4.class) | |
public class MainActivityUITest { | |
@Rule | |
public MockitoRule mockitoRule = MockitoJUnit.rule(); | |
@Rule | |
public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class, true, false); | |
@Mock | |
MainActivityComponent.Builder builder; | |
@Mock | |
Utils utilsMock; | |
private MainActivityComponent mainActivityComponent = new MainActivityComponent() { | |
@Override | |
public void injectMembers(MainActivity instance) { | |
instance.mainActivityPresenter = new MainActivityPresenter(instance, utilsMock); | |
} | |
}; | |
@Before | |
public void setUp() { | |
when(builder.build()).thenReturn(mainActivityComponent); | |
when(builder.activityModule(any(MainActivityComponent.MainActivityModule.class))).thenReturn(builder); | |
ApplicationMock app = (ApplicationMock) InstrumentationRegistry.getTargetContext().getApplicationContext(); | |
app.putActivityComponentBuilder(builder, MainActivity.class); | |
} | |
@Test | |
public void checkTextView() { | |
String expectedText = "lorem ipsum"; | |
when(utilsMock.getHardcodedText()).thenReturn(expectedText); | |
activityRule.launchActivity(new Intent()); | |
onView(withId(R.id.textView)).check(matches(withText(expectedText))); | |
} | |
} |
Step by step:
-
We provide Mock of
MainActivityComponent.Builder
and all dependencies which have to be mocked (justUtils
in this case). Our mockedBuilder
returns custom implementation ofMainActivityComponent
which injectsMainActivityPresenter
(with mockedUtils
object in it). - Then our
MainActivityComponent.Builder
replaces the original Builder injected inMyApplication
(line 28):app.putActivityComponentBuilder(builder, MainActivity.class);
- Finally test — we mock
Utils.getHardcodedText()
method. Injection process happens when Activity is created (line 36):activityRule.launchActivity(new Intent());
and at the and we’re just checking the results with Espresso.
And that’s all. As you can see almost everything happens in MainActivityUITest class and the code is pretty simple and understandable.
Source code
If you would like to test the implementation on your own, source code with working example showing how to create Activities Multibinding and mock dependencies in Instrumentation Tests is available on Github: Dagger2Recipes-ActivitiesMultibinding.
Thanks for reading!
Author
Miroslaw Stanek
Head of Mobile Development @ Azimo Money Transfer
If you liked this post, you can share it with your followers or follow me on Twitter!