Pure Dependency Injection - How to implement it
https://softwareengineering.stackexchange.com/questions/354465
-
16-01-2021 - |
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.
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 Activity
s when activities are constructed by Android for us? It's not so straightforward, but the closest correllation to constructor parameters are Bundle
s that you can attach on Intent
s. 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.