Practicing craftsmanship in Android is hard. Really hard. The framework imposes itself in a number of ways that make it
difficult to write well-crafted, testable code. The official examples don't do anything to help this, as they weren't designed to show Android development done well, they were designed to simply show it "done". Fear not though, as others have encountered the same difficulties you're probably now facing and they've determined that where there is a will, there is a way. They've figured out that it is indeed possible to use good engineering principles such as Dependency Injection and Mocking in Android development.
Having said that I've made it a point to incorporate these practices into my app development and found a lot of success in doing so. The combination of Robolectric, Dagger for dependency injection, and Mockito for mocking collaborators is powerful, but it gives you a lot of flexibility. I'd like to share this setup with you here in the hopes that it will help you design more testable, maintainable, and flexible solutions.
I've
previously written about using Robolectric to stub portions of the Android framework, so lets move on to incorporating dependency injection into our apps.
Dagger Setup
Dagger is an open-source dependency injection framework for Java applications. It is not Android-specific, but has certainly become the leading DI framework of choice for Android. Dagger is currently in its 2.x version, however Dagger 1.x is still dominate and it is the version I'm currently using. The following setup will be for Dagger 1.2.
Conceptually Dagger provides Dependency Injection by organizing dependencies into modules, which are used to create an object graph. This object graph is traversed at the time of injection to determine what dependencies should be injected, and in what order. Let's take a look at what this means in concrete terms in your Android application, shall we?
Once we have the necessary build dependencies we can start integrating Dagger in.
In build.gradle:
dependencies {
//Other dependencies
//Dagger
compile 'com.squareup:javawriter:2.5.0'
compile ('com.squareup.dagger:dagger:1.2.2') {
exclude module: 'javawriter'
}
compile ('com.squareup.dagger:dagger-compiler:1.2.2') {
exclude module: 'javawriter'
}
}
The next step we take is to extend the Application class and provide some scaffolding for the injection process:
In MyApplication.java:
package com.example.jameskbride;
import dagger.ObjectGraph;
public class MyApplication extends Application {
protected ObjectGraph objectGraph;
@Override
public void onCreate() {
super.onCreate();
createObjectGraph(getModules());
}
public List<Object> getModules() {
List<Object> modules = new ArrayList<Object>();
return modules;
}
public void inject(Object context) {
graph.inject(context);
}
public void createObjectGraph(List<Object> modules) {
graph = ObjectGraph.create(modules.toArray());
}
}
Let's take a moment to break down what we've set up here. First, we've introduced a protected member variable here, which is of type ObjectGraph. This variable is the key to making Dagger useful, as it provides the mechanism for actually performing the injections we need. The rest of the logic we've introduced is implementation-specific, but it should cover 99% of the use-cases we anticipate. In the onCreate() method we make a call to a method we introduced, createObjectGraph(), which takes a list of objects (nominally dependency modules, which we'll get to in a moment) and invokes the static method ObjectGraph.create() to instantiate the object graph. We've also introduced a public inject() method, which performs the actual injection via the object graph.
Before going too much further we need to satisfy Android by insuring it knows about our overridden Application class. To do this we need to modify our AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.jameskbride">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<!-- snip -->
</application>
</manifest>
This setup provides the framework we need to inject modules, which which don't actually have at the moment. Let's fix that now. The first thing we're going to do is to create a module to provide injection of some of our custom business logic. First, suppose we have the following interface and class:
package com.example.jameskbride;
public interface SpeakerInterface {
public String sayHello();
}
package com.example.jameskbride;
public class Speaker implements SpeakerInterface {
@Override
public String sayHello() {
return "Hello, world!";
}
}
Given this class, lets create a module to provide injection for it.
package com.example.jameskbride;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module(
injects = {
MyActivity.class
}
)
public class MyModule {
@Provides
@Singleton
public SpeakerInterface getSpeaker() {
return new Speaker();
}
}
In this class we have several annotations provided by by Dagger and
JSR330, the Java specification for dependency injection. The @Module annotation provides a way to configure the module, and @Provides is the mechanism for telling Dagger where the dependency should be constructed when necessary. Now that we have a module let's go back to our Application class and plug it in there.
public class MyApplication extends Application {
//Snip
public List<Object> getModules() {
List<Object> modules = new ArrayList<Object>();
modules.add(new MyModule());
return modules;
}
//Snip
}
Almost done. Now we just need to actually inject into our class where the dependency will be used. Let's create an activity which will use our dependency.
In MyActivity.java:
package com.example.jameskbride;
import android.app.Activity;
import android.os.Bundle;
import javax.inject.Inject;
public abstract class MyActivity extends Activity {
@Inject
SpeakerInterface speaker;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApplication)getApplication()).inject(this);
}
}
Notice the @Inject annotation on our SpeakerInterface member variable, as well as the fact that the variable has package scope. The package scoping is important, as injection via Dagger will not work if the injected dependency is private. Also notice that we finally call MyApplication.inject() within the onCreate() method; this is what causes the injection to occur in the first place.
This was quite a lot of work! We should now be able to make calls to the speaker within our Activity. As you can see there is quite a bit of overhead involved in the setup of Dagger, and this is just for the production side of things. Let's take a look at the test setup now.
Test Setup
The testing side of the Dagger setup is almost identical to the production setup. In fact, it simply involves overriding our custom Application class and providing test doubles of our modules. First, the Application class:
package com.example.jameskbride;
import java.util.ArrayList;
import java.util.List;
import dagger.ObjectGraph;
public class MyTestApplication extends MyApplication {
private MyTestModule myTestModule;
@Override
public List<Object> getModules() {
List<Object> modules = new ArrayList<Object>();
modules.add(getMyTestModule());
return modules;
}
public MyTestModuleModule getMyTestModule() {
if (myTestModule == null) {
this.myTestModule = new MyTestModule());
}
return myTestModule;
}
}
Here's we've created an Application class which extends our custom application, MyApplication. Notice we are loading a test version of MyModule, shown below:
package com.example.jameskbride;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.mockito.Mockito.mock;
@Module(
injects = {
MyActivity.class
}
)
public class MyTestModule {
private SpeakerInterface mockSpeaker;
public MyTestModule() {
mockSpeaker = mock(SpeakerInterface.class);
}
@Provides
@Singleton
public SpeakerInterface getSpeaker() {
return mockSpeaker;
}
}
You'll notice that the test module is almost identical to the original. In this case though we are substituting the real Speaker implementation with a Mockito mock. Don't forget to add the appropriate test dependency for Mockito in your build.gradle:
dependencies {
// Other dependencies
testCompile 'org.mockito:mockito-core:1.10.19'
}
Now we're ready to write tests which use our Dagger-provided dependencies. Let's write our first test to drive in some functionality.
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
private ActivityController<MyActivity> activityController;
private MyActivity activity;
@Test
public void whenTheActivityIsCreatedThenTheTitleIsSet() {
SpeakerInterface mockSpeaker = ((MyTestApplication)activity.getApplication()).getMyTestModule().getSpeaker();
String expectedTitle = "I hate hello world examples!";
when(mockSpeaker.sayHello()).thenReturn(expectedTitle);
activityController = Robolectric.buildActivity(MyActivity.class);
activity = activityController.create().start().get();
assertEquals(expectedTitle, activity.getTitle());
verify(mockSpeaker).sayHello();
}
}
Run the test and watch it fail. Once we've watched it fail correctly we can implement the code to make it pass:
package com.example.jameskbride;
import android.app.Activity;
import android.os.Bundle;
import javax.inject.Inject;
public abstract class MyActivity extends Activity {
@Inject
SpeakerInterface speaker;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApplication)getApplication()).inject(this);
setTitle(speaker.sayHello()); //Here is the line which makes the test pass.
}
}
The test should now pass, and we are officially configured to use Dagger in both production and test!
Again, there is obviously a lot of overhead involved in the setup of Dagger, but this will pay off in the long run as your application grows. I hope this has been useful, and feel free to send me any questions you might have! Thanks!