Question

I'm doing an Android project for an online course. I would like to use DI in that project, so I started to use dagger2, but now I started to have typical newbie problems that are holding me back.

As the project deadline is coming, I decide to abandon dagger2 for this project, but I still like to use some sort of DI.

So I came to this article:

Pure Dependency Injection or Poor Man Dependency Injection

But I did not understand exactly how to implement this in Android. I could start to avoid create instances of my objects in the constructor or inside the dependent class, but somewhere it has to be instanced.

I'm looking for some idea or implementation strategy for this "Pure DI" in Android. If possible, a well-implemented example.

Was it helpful?

Solution

You've probably finished your course by now, but in case you are still searching, or someone else is: rolling your own DI is actually very simple on Android once you know how to do it.

Creating the Dependency Graph

I usually first start with a custom Application class (don't forget to register it in the android manifest), this class will live throughout the lifecycle of your android app and is where your code can access its dependencies from. Something like this:

public class CustomApp extends Application {

  private static ObjectGraph objectGraph;


  @Override
  public void onCreate() {
    super.onCreate();

    objectGraph = new ObjectGraph(this);
  }


  // This is where your code accesses its dependencies
  public static <T> T get(Class<T> s) {
    Affirm.notNull(objectGraph);
    return objectGraph.get(s);
  }


  // This is how you inject mock dependencies when running tests
  public <T> void injectMockObject(Class<T> clazz, T object) {
    objectGraph.putMock(clazz, object);
  }

}

(Affirm.notNull() just blows up if something is null, you don't need to use it). The actual dependencies are all in the ObjectGraph class which basically looks like this:

class ObjectGraph {

  private final Map<Class<?>, Object> dependencies = new HashMap<>();

  public ObjectGraph(Application application) {

    // Step 1.  create dependency graph
    AndroidLogger logger = new AndroidLogger();
    Wallet wallet = new Wallet(logger);

    //... this list can get very long


    // Step 2. add models to a dependencies map if you will need them later
    dependencies.put(Wallet.class, wallet);

  }

  <T> T get(Class<T> model) {

    Affirm.notNull(model);
    T t = model.cast(dependencies.get(model));
    Affirm.notNull(t);

    return t;
  }

  <T> void putMock(Class<T> clazz, T object) {

    Affirm.notNull(clazz);
    Affirm.notNull(object);

    dependencies.put(clazz, object);
  }

}

Usage

Now wherever you are in your app (in an activity for example) you can inject your dependencies like this:

private Wallet wallet;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    wallet = CustomApp.get(Wallet.class);

    //...
}

You can use Dagger in a few different ways, but the nearest Dagger2 equivalent would be something like this:

AppComponent appComponent = CustomApp.getAppComponent();
Wallet wallet  = appComponent.getWallet();

Scoped dependencies

What you would be injecting here would be a class that has application level scope. If you just want a local scope object that only exists for as long as you keep a reference to it in your view or activity, you do exactly the same thing but what you inject is a factory class:

In ObjectGraph:

WalletFactory walletFactory = new WalletFactory(logger);

In your Fragment for example:

Wallet wallet = CustomApp.get(WalletFactory.class).getNewWallet();

Testing

All this is done so that you can test your view layer code easily. For example if you want to run an espresso test, you create the application, but before showing the activity, you replace the wallet instance with a mock:

CustomApp.injectMockObject(Wallet.class, mockedWalletWith100Dollars);

This is the wallet instance that will then be picked up by the rest of your code during the test.

In some ways this style of DI is not as flexible as Dagger2, but I think it is a lot clearer - just no need to make DI complicated it's actually quite a basic thing. This style also often results in less boiler plate than using a DI framework (once you include component and module classes).

Full examples

I wrote something similar for 5 sample apps as part of a framework I published (wanting to keep the samples as widely accessible as possible - not everyone likes Dagger). You can see the full examples here: https://github.com/erdo/asaf-project/blob/master/example01databinding/src/main/java/foo/bar/example/asafdatabinding/ObjectGraph.java

OTHER TIPS

For plain Java classes in Android pure DI is implemented as usual with constructor parameters. For example:

interface UserService extends Parcelable {
    User getUser(String name);
}

class DbUserService implements UserService {
    private final DbHelper db;

    public RestUserService(DbHelper db) {
        this.db = db;
    }

    User getUser(String name) {
        // use db to get user by name
    }
}

The complexity of doing pure DI in Android comes with the framework integration. How do we do DI with Activitys when activities are constructed by Android for us? It's not so straightforward, but the closest correllation to constructor parameters are Bundles that you can attach on Intents. Let's use an example to demonstrate how this would work:

Say I have an activity UserActivity that fetches a user from a UserService and displays it in a view. I want to inject the UserService into UserActivity to make it more testable (along with all the other benefits of DI).

class UserActivity extends Activity {
    private Optional<UserService> userService = Optional.empty();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // get UserService from bundle
        userService = Optional.of(getIntent().getExtras().getParcelable<UserService>("userService"));
        setContentView(R.layout.activity_user);
    }

    @Override
    protected void onResume() {
        super.onResume();
        userService.map(us -> us.getUser("Bill"));
    }
}

Since Intent implements Parcelable, we can construct our object graph at the composition root like so:

UserService userService = new DbUserService(...);
Intent userActivityIntent = new Intent(this, UserActivity.class);
userActivity.putExtra("userService", userService);
Intent mainIntent = new Intent(this, MainActivity.class);
mainIntent.putExtra("userActivityIntent", userActivityIntent);
startActivity(mainIntent);

The limitations of this approach is that passing arguments relies heavily on Parcelable. All your dependencies must be marshallable. Many Android's classes don't (and shouldn't) implement Parcelable, so you must design your application in a way that you don't need to pass them around.

Licensed under: CC-BY-SA with attribution
scroll top