Dagger & The Object Graph
The Dagger library is a dependency injection framework that will go a long way in helping us fulfill requirements 1 and 2. Here are the specific capabilities it grants us:
- In most cases, the app does not instantiate any classes outside of the object graph and its associated wrappers. Of course, this is a huge necessity for any amount of unit testing.
- It decouples the creation of the MVC components for each screen. The graph creates controllers, injects them with regular DI, and the factory for creating and controlling the intended view is also provided similarly.
- The graph provides all of the factories and forwarders as the facade into the classes that would otherwise not work during a unit test or would require in-place instantiation.
This practice underscores a fascinating concept. The same classes which inherit from the Android API, and we are making such an effort to sideline, can still benefit from Dagger and follow best practices by building the object graph and late injecting the components into itself. In essence, we aren’t calling these classes a lost cause because it breaks our foundational principles of unit testing. We follow best practices as much as possible, for as long as possible, until it is impossible.
Dependency Injection Architecture
There are several moving parts to this aspect, which I’ve thus far just called “Dagger.” While there are many ways to arrange this tool to work for your project, here is a pattern that I have found works well.
While the names in the above diagram show exactly how the sample application uses DI, focus on the concept rather than the specific words. You will have different classes, more modules and components, and more varied user interfaces for your application. Look at the colors and arrows, instead. Here is how I see this diagram:
wrapperpackage contains all of the factories and forwarders. This package gets module a dedicated module, and the
WrappersModulecreates each instance of a factory or forwarder class.
- All other modules encapsulate a general concept of related items. For example,
MainModuleholds all of the things related to the
MainActivity, such as a reference to the class itself,
context, and anything else subsequent screens and utility classes may need to know about their parent activity. These names are highly context-specific and may vary significantly in your application. If, for instance, I had more networking going in inside of my app, I may generalize the
HttpServiceModuleinto something like
NetworkModuleand use it to build a more broad set of classes.
- The modules are injected into whatever components require them. It’s just standard Dagger practice.
DaggerInjectoris just an object (in Kotlin terms), or a static class (in Java terms). Whenever it receives a request to build an object graph, it builds it, caches the graph in static memory, and returns a reference of that graph to the caller. Subsequent calls to the
DaggerInjectorsimply reference the cache to use DI.
In my applications, I usually build one component per activity. So, since this application only has a single activity, there is just one component. However, should this application grow to include more activities, they would still flow through the
DaggerInjector. It would just have more functions to build the requested component and cache it.
We follow best practices as much as possible, for as long as possible, until it is impossible.
As far as modules go, I follow a loose set of rules. I imagine that I have
N number of buckets and
M things to go inside those buckets. Let’s say that
M is always much greater than
N. Now, given my limited number of buckets, how I would categorize my
M items so that they can logically fit into those
N buckets? Now replace “buckets” with “modules” and “things” with “classes I need to inject.” Get the idea?
Before moving onto the next concept, there is one last detail I left out of the first diagram which is important to discuss.
MainActivity is a prerequisite for building the object graph. Down the road, I know that several modules and screens may need access to it and the ever-ubiquitous
context that it provides. Here is how I provide that activity to the graph:
Specifically, this is the entire flow from the time that the
MainActivity requests an object graph, to the time it receives all of its dependencies:
- Android invokes
onCreate()on the activity.
buildMainComponent(activity)is invoked on the
DaggerInjector. This method is a static function on the injector which builds the graph, using the supplied reference to the
- The builder gives the activity to whichever modules require it as a dependency.
- The builder creates the graph and caches it as a static property on the injector.
buildMainComponent()returns a reference to that graph and the
MainActivityuses it to inject the missing classes into itself.
Activity classes cannot be instantiated by the user, the dependencies are late injected in
onCreate(). This process happens as early as possible in the activity lifecycle to ensure all downstream procedures have the tools they need to function.
To see how this works in practice, look at the code here: Android Dagger Setup.
MVC Base Classes
One of my favorite aspects of this particular pattern is that you are not dependent on any third-party libraries to do the heavy lifting for you. Other architectures, such as Redux or Flux, can have so much overhead that you need to use a library to lighten the load. Not so with MVC! This bad boy boils down to 5 classes, 2 of which are just interfaces.
I won’t go through each class in detail, but will provide a general explanation of the high points:
ViewMvcThe base class for all MVC interfaces and classes. It merely holds onto a reference of the inflated view.
BaseViewMvcAdds additional functionality to the root view saved in the
ViewMvc, such as finding the view’s
contextor locating sub-views within the screen with
BaseViewMvc, this class adds the ability to register listeners to respond to custom events, such as button taps, swipe gestures, or anything else a view may wish to dispatch.
ViewMvcFactoryGenerates all of the
ViewMvcclasses for each screen and injects the necessary dependencies into each view.
That’s it. If you are interested in seeing what these look like, take a look at the code here. As you will see later, these classes help us establish a pattern we use to exercise requirements 1 and 3.
The last foundational point I’ll discuss is the JaCoCo ignore filter. Having done everything possible to isolate and prepare our classes for unit testing, there are just some less critical areas that need to be left behind. This is the final aspect of pattern 3 that our setup helps us achieve. Here is the code coverage exclusion filter:
What kinds of classes didn’t make the cool kids club and got excluded? These filters cover several categories:
- Activities: We’ve already spoken in depth about this.
- Models: These files don’t contain any code worth testing since it is just a data container. JaCoCo still likes to cover these things unless told otherwise.
- Dagger and DI: For obvious reasons, any code related to DI, factories, and forwarders are excluded. This also includes the
- Auto-generated code: If I (or my teammates) didn’t write the code, then we don’t bother unit testing it. The Android build process, Dagger, and the Android Navigation Component collectively create a lot of extra classes that we can forget about.
To see how this works in practice, look at the code here: App Module Gradle File.