Paul Brower bio photo

Paul Brower

Android development for fun and profit.

Email Twitter Github Stackoverflow Codementor

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!