See code and installation instructions at https://github.com/browep/DeadSimpleDependencyInjection.
Dependency Injection, A quick definition: Instead of a Class instantiating dependencies itself it uses an Injector to supply them. A much longer explanation can be found at http://www.theserverside.com/news/1321158/A-beginners-guide-to-Dependency-Injection
Dependency Injection can be very for useful testing and development. For example, let’s say you have an Activity that calls out to the network in a few places using a NetworkAdapter. If you wanted to test this Activity you would need some way to handle these network calls. You may want them to always succeed or always fail, or maybe return predictable data from a test resource file. Either way, the implementation of NetworkAdapter that you use to access the data for production is not suitable for testing. With DI you can change the NetworkAdapter field to be a MockNetworkAdapter that always fails, or succeeds, or returns test data.
The State of Dependency Injection on Android today
Dagger from Square is really the only option right now for DI on Android. This is unfortunate because Dagger is one of the most painful libraries I have ever worked with. I have tried to use it to make my own development easier on no less than 3 projects. I have spent twice as much time wondering why it isn’t working than I have would have saved by using it. And I say “wondering” as the ability to debug or even understand what the Dagger code is doing is near impossible. You can thank code generation for that. Be wary of any library that tries to hide it’s complexity through tons of annotations, it’s usually more pain than it’s worth.
/rant
So I decided that DI should be easy, useful, and understandable.
Enter Dead Simple Dependency Injection
Using DSDI is straightforward. The entirety of the logic is encompassed in a single file, DependencySupplier.java. In it’s essence, the DependencySupplier does the following:
- receive an object in it’s
.inject(Object obj)
method - iterate over the fields in that object’s Class
- gets the type for that field
- gets an object of that type ( by calling the abstract
supply(Object injectee, Class injectionClass)
method that is implemented by the subclass) - set that field to the supplied object.
- be able to be understood and modified
Any class that needs dependencies injected ( an Activity with a NetworkService or a DataAccessObject ) just needs to call dependencySupplier.inject(this)
and its fields will be filled with whatever needs it.
That’s the core of it. Let’s walk through an example.
See Getting Started for installation instructions.
Let’s say we have our SampleActivity.java that we want to use DI for. It has a NetworkAdapter that it uses to access server content. The NetworkAdapter uses Retrofit as a backend. When we test this we don’t want our calls to actually go out to the server, instead we want the content to come from a local file in the test resources and always to succeed.
Let’s write the SampleActivity.java, it’s a pretty simple class:
import javax.inject.Inject;
public class SampleActivity extends AppCompatActivity {
@Inject
private NetworkAdapter networkAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((SampleApplication) getApplicationContext()).getDependencySupplier().inject(this);
}
@Override
protected void onResume() {
super.onResume();
networkAdapter.listRepos("browep", new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
// do something with the list of Repos
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
// handle the call failure
}
});
}
}
Notice two things. First, the private NetworkAdapter networkAdapter
has an @Inject
annotation on it and we are calling ((SampleApplication) getApplicationContext()).getDependencySupplier().inject(this);
with the Activity as an argument. That’s all the code you need for the injectee. Let’s look at the supplier end. We will keep the reference to the DependencySupplier in the Application object (SampleApplication in our case).
public class SampleApplication extends android.app.Application {
public static final String DSDI_INJECTOR_CLASS = "dsdi.injector_class";
private DependencySupplier dependencySupplier;
private static SampleApplication INSTANCE;
@Override
public void onCreate() {
super.onCreate();
// create a singleton so we can access it from a static context
INSTANCE = this;
// setup dependency injector
dependencySupplier = setupDependencySupplier();
}
public static SampleApplication getInstance() {
return INSTANCE;
}
/**
* here we instantiate the DependencySupplier. We look for a class in the
* System object ( which is what we set when we want to use a Test DI) and
* default to the Prod DI if we dont find one
*/
protected DependencySupplier setupDependencySupplier() {
// get the name from the system props
String className = System.getProperty(DSDI_INJECTOR_CLASS);
// default to production supplier if none other specified
if (TextUtils.isEmpty(className)) {
className = ProductionDependencySupplier.class.getCanonicalName();
}
return DependencySupplier.initializeSupplier(className);
}
public DependencySupplier getDependencySupplier() {
return dependencySupplier;
}
}
There are some important decisions made here so let’s talk about them. We construct the DependencySupplier here instead of somewhere else because the Application object is the easiest to reference throughout the app. We also look for a fully qualified class name for a DependencySupplier and use that if it’s present. This will come in handy later in testing.
Now let’s look at our ProductionDependencySupplier
.
public class ProductionDependencySupplier extends DependencySupplier {
private NetworkAdapter networkAdapter;
private Dao dao;
public ProductionDependencySupplier() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
networkAdapter = new NetworkAdapter(retrofit.create(Server.class));
}
@Override
public Object (Object o, Class aClass) {
if (aClass.equals(NetworkAdapter.class)) {
return networkAdapter;
} else {
throw new IllegalArgumentException("could not supply: " + aClass);
}
}
}
Its pretty simple in that it only needs to supply a NetworkAdapter instance. In a larger app we would have a much bigger supply
that returns things like Analytics, Data Access, Disk Access, Encryption Manager, and any other thing our app would need.
So this is all great but the benefits of DI are not obvious yet. We haven’t done anything interesting and we are only supplying what we would have created anyway. Let’s now use DI to help up with testing.
Using DeadSimpleDependencyInjection in testing
For this example we are using the latest of what Google recommends for testing, Espresso. See https://developer.android.com/topic/libraries/testing-support-library/index.html#Espresso for how to get started in testing. There are a few key things we need to do in order to get things working. One is override the instrumentation test runner that is specified in the build.gradle
to use our custom runner so we can run code before the Application
object is created.
// the rest of the build.gradle has been removed for brevity
android {
defaultConfig {
testInstrumentationRunner "com.github.browep.dsdi.sample.CustomInstrumentationRunner"
}
}
Let’s look at CustomInstrumentationRunner
public class CustomInstrumentationRunner extends AndroidJUnitRunner {
@Override
public void onStart() {
super.onStart();
}
@Override
public void callApplicationOnCreate(Application app) {
System.setProperty(SampleApplication.DSDI_INJECTOR_CLASS, TestDependencySupplier.class.getCanonicalName());
super.callApplicationOnCreate(app);
}
}
Also a simple class. The method callApplicationOnCreate
will be called, you guessed it, before Application.onCreate
. This is the best way to have code run before any tests are. You can see we are setting that System property that the SampleApplication
is looking for to the TestDependencySupplier
class. Let’s take a look at it.
public class TestDependencySupplier extends ProductionDependencySupplier {
public TestDependencySupplier() {
super();
}
@Override
public Object supply(Object o, Class aClass) throws IllegalArgumentException {
if (aClass.equals(NetworkAdapter.class)) {
return new MockNetworkAdapter();
} else {
return super.supply(o, aClass);
}
}
public static class MockNetworkAdapter extends NetworkAdapter {
public MockNetworkAdapter() {
super(null);
}
@Override
public void listRepos(String user, Callback<List<Repo>> callback) {
List<Repo> repos = new LinkedList<>();
Repo repo = new Repo();
repo.name = "Test Repo1";
repos.add(repo);
repo = new Repo();
repo.name = "Test Repo2";
repos.add(repo);
repo = new Repo();
repo.name = "Test Repo3";
repos.add(repo);
callback.onResponse(null, Response.success(repos));
}
}
}
This looks similar to the ProductionDependencySupplier
. But instead of a Retrofit service we are creating a MockNetworkAdapter
that returns a static list of repos. The test for the Activity can be straightforward.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SampleActivityTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
SampleActivity.class);
@Test
public void response() throws Exception {
// test the response from the NetworkAdapter
}
}
That is a full example of DI. We have used it throughout the app and now in a test environment. Go forth and inject!