In my previous post I showed how we can structure our Views so that they’re easier to reason about and more deterministic by thinking about them as a function of state. I did that by ensuring two things:
First, the View’s own state — text draw on screen, selected buttons, loaded images — only gets updated through a redraw method.
Second, the View only interacts with the outside world through an Interactor.
That’s already a huge step up, in my opinion.
There’s room to improve even further, though, if we think about the number of responsibilities the view has.
I’m a big fan of the Single Responsibility Principle, which generally states that a class should have one and only one reason to change. Looked at from that lens, our view has three responsibilities:
- Ensure it renders correctly on screen. The ContactView is pretty simple and static, but more advanced views can have animations, effects, etc.
- Assign the values it gets from a ViewState to the corresponding subview fields
- And connect the Interactor with the corresponding button click listeners
There’s a further problem if we look at it from a business or functional perspective. What if we want to reuse the same view with different data sources? Or what if we want to reuse the same view but have it perform different actions in different parts of an app? How can we achieve that?
Surely there are many good answers to the above two questions. I’m going to show a way that I’ve been tinkering with in my spare time that’s influenced by the way Components in React Native interact with State in Redux.
First, let’s recap how we laid out our View:
And what code we have so far:
Decoupling responsibilities: redrawing
How do we start to break up this View, then?
We can start by taking the first responsibility and abstracting it away to its core components.
We can then provide a default implementation for a ContactView, based off the work we just did.
In a way, we’re back where we first started. But what’s more important is the above View can focus on just displaying information on screen in a nice way, whether by animating the followButton or highlighting the phoneTextView.
But how can the view actually do anything at all now? How can it even redraw itself?
Well, we can start by redefining the original ContactView.ViewState as its own independent entity. It no longer is the view state of a particular view but rather a view state that represents some form of contact information.
The information within stays the same and the abstraction on top of business layer models that this view state represents stays the same.
Here is where it gets interesting! We can take the ContactView and the ContactViewState — two independent entities at this point — and join them via a third one: redraw as a function from view state to view.
The above is really powerful.
By moving the redraw method outside of the view we’ve solved two problems in one go: we’ve removed the extra responsibility from the ContactView and we’ve also opened the possibility of reusing ContactView in multiple parts of our app. All we need to do is define different redraw methods, that take different view states, and join them with the same view.
What’s more, testing UI components with Espresso is still as straight forward as before, only instead of testing the view’s behaviour we are testing a particular redraw function in relation to a view and a state.
Decoupling responsibilities: interaction
At this point we’ve simplified our view tremendously and created a suite of redraw functions to meet our needs. But how can we make the view load itself or perform some sort of action?
Let’s go back to our initial Interactor:
It’s clear that it has two responsibilities: loading a contact and adding / removing it from a list of favourites. We want our code to adhere to the Single Responsibility Principle as much as possible so we can start by creating two independent Interactors, one for each responsibility.
We can apply exactly the same principle as with the redraw methods to create a series of binding methods. Each method will bind some Interactor to some view.
Once more, when we test all of this, it’s not a view’s interaction we’re testing, but the relationship between an Interactor and a view. As an added benefit, the tests themselves become simpler and more concise.
One last thing about interactors: they’re petty generic.
One one side, that’s really good. We’re totally shielding our views from any business layer knowledge like contact ids, etc.
One the other side, it’s not so great because we don’t exactly know what contact to load or to follow anymore.
How do we solve that? As usual, there’s many options. We defined interactors as interfaces so maybe their implementation can hold those details. Whilst that’s totally valid, I like to go for a more functional approach, where the actual implementation of these interactor interfaces is a function.
Or, to be more precise, a function that returns an anonymous implementation of an interactor, based off some view model and a contact id. The implementation may vary, of course, but the general idea is the function acts as a factory that correctly implements the interactor and connects it to the broader code base.
Bringing it all together
Finally let’s bring all of these together. We have our views, our interactors, our independent redraw methods and our independent bind methods.
We can start by placing our interactors into an easily accessible Dependency Injection object. We can place repositories, view models, etc in here as well.
The above example is pretty basic but it’ll do the job. You can use Dagger 2 and other similar DI frameworks to achieve a better result.
Finally in an Activity or Fragment we can join the views with the interactors and the redraw methods.
The above is alright and if we want, we can leave it at that. But it also puts a lot of responsibility on the Activity to hook up all these pieces — a responsibility that can quickly grow out of hand when we add multiple views.
Coming back to the beginning of the article, where I mentioned React Native and Redux, there’s a concept there called a connected component. It’s basically the result of connecting a normal component — essentially our view — to the wider redux environment. We can take that concept and create a ContactComponent of our own:
We’ve essentially moved all of that responsibility that we placed inside the Activity into another function that acts as a factory for our view.
Finally, the Activity just becomes this:
To sum up, this is most of the code that we’ve written so far:
This article is one of a multi-part series exploring State, UI and Redux and how we can leverage knowledge of these topics to write more modular and testable code.