Dependency Inversion is an important principal that's key to writing clean, testable, and easily maintainable code. As a part of our series on the DebugRank app, we'll examine utilizing the dependency inversion principal in Java via the dependency injection pattern. We'll also take a look at the Dagger2 framework to show how you can minimize dependency injection boiler plate code and maintain dependency lifespan/scope in your Android app. Finally, we'll cover how to evaluate if Dagger2 is the right solution for you.
What is Dependency Inversion
Dependency inversion is part of the SOLID principals, which are some core principals to help you write clean, maintainable, and testable code. The dependency inversion principal is the idea of decoupling all dependencies from each other, depending upon abstracting rather than concretion, or just basically wrapping all dependencies in interfaces and only talking to those interfaces. By depending only on abstraction, I can easily swap out the implementation details (development vs production builds) and mock the dependency during unit testing.
What is Dependency Injection
Dependency injection is an implementation of the dependency inversion principal. Essentially you inject dependencies into a class simply by passing an already initialized implementation of said dependency, usually through the constructor.
Before Dependency Injection
In the gist below I've decoupled the actual syntax highlighting logic from my model via the PrettifySyntaxHighlighter service, but my model is still dependent on a concrete implementation (see line 4 of CodeModel).
After Dependency Injection
Now, I'll take it a step further by abstracting the syntax highlighting behind an interface and my model will receive the interface via the constructor. This is dependency injection. This means my model never knows about the actual implementation of my syntax highlighting service. This allows me to create and maintain the concrete implementation somewhere else entirely.
Why abstract concrete implementation dependencies?
The biggest reason for this abstraction is the improved testability brought by minimizing the code my unit tests actually test. Now when I need to write unit tests for my CodeModel, I can simply mock the syntax highlighter dependency to ensure my unit tests only test the logic in CodeModel (which does nothing but maintain a simple cache). Refer to the gist below.
Android Dependency Injection / Dagger2
We talked about how our CodeModel receives dependencies via the constructor, but where do those dependencies get initialized? Who maintains the reference? What is the lifespan/scope of the dependency? How does my Activity/Fragment get a local reference to these dependences?
Non-"Dependency Injection Framework" Solution
One solution is to utilize the Factory Pattern to deal with dependency creation. Then I can maintain the reference to my dependencies in my Activity. You can also place this logic in your Application class to maintain application scoped dependencies that are maintained in memory for the lifespan of your App.
Who maintains the reference? The reference is maintained by either my Activity, Fragment, or service for Activity-Scoped, or my Application for Application-Scoped. Ultimately, I'll have to write code for creation. What is the lifespan/scope of the dependency? Activity-Scoped will be alive until the Activity is destroyed. Application-Scoped will be alive until the Application is destroyed. I'll have to write code for creation and destruction. How does my Activity/Fragment get a local reference to these dependences? The Activity/Fragment calls the necessary factories to initialize local references, or retrieves them from the Application.
The Dagger2 Solution
Dagger2 is a dependency injection framework that helps reduce boiler plate code, this code being factories, singleton / scope, and consuming dependencies in my Activity/Fragment. In addition to reducing code it helps modularize my dependencies by grouping them into "modules" that can be reused by other modules. This allows me to create "components" to tie multiple modules to an Activity, Fragment, or Service. Who maintains the reference? Dagger2 will keep the dependency alive depending on what scope tag you utilize at the Module level or Component level. What is the lifespan/scope of the dependency? Lifespan is handled entirely by Dagger2 depending on the scope tag utilized at the Module or Component level. How does my Activity/Fragment get a local reference to these dependences? You can utilize the @Inject tag to obtain the reference, no information / code necessary for maintaining the original reference or lifespan.
Setup the Module
Dagger2 lets me setup "modules" that hold multiple dependencies, ideally I'd place related dependencies together.
Set up the Component
Now I need to tie modules to an Activity, Fragment, or Service using a component. This is necessary in order to inject the dependencies later.
Setup Dagger2 inside App onCreate
In my App's onCreate method I initialize the dagger modules and associate them with the dagger components.
Injecting dependencies into Activity
With the component set up, I can finally inject some dependencies into my Activity, Fragment, or Service by utilizing some simple annotations and adding an inject line.
The road to using Dagger2
1. Understand Dependency Inversion First, you must understand the dependency inversion principal and the benefits (maintenance, readability, testability) of relying on abstraction for dependencies. Ultimately, you should understand and implement all of the SOLID principals before moving onto dependency inversion. 2. Understand / Implement Dependency Injection Once you feel like you adequately understand Dependency Inversion, you can move onto an implementation of the principal, dependency injection via the constructor for instance. 3. Evaluate project size & complexity Once you master implementing the principal, the next step is to evaluate the size (# of dependencies) and complexity (scope lifespan necessary) of your project. For smaller projects Dagger2 is probably unnecessary and overkill, and might even end up making you write more code. But if your project is large (a multi level dependency tree) with the possibility for App/Activity/User scope then Dagger2 is the solution on Android.
It's important to understand that Dagger2 is not dependency injection, but a tool/framework that sits on top of your code that already utilizes Dependency Injection. Dagger2 helps you minimize boiler-plate code and help with dependency lifespan to improve your Android apps.