A Refined Approach To MVC Architecture in Android Part 3 — Establishing Loose Coupling Between Activity and View Component Implementation
In PART 1 we saw how Activity and Fragment is unsuitable to act as the view components in MVC. Then in PART 2 we refactored our StackOverflow client app extracting all the view logic from QuestionsListActivity and QuestionsDetailActivity and put it into QuestionListMvcViewImpl and QuestionDetailMvcViewImpl respectively.
Looking take a look at QuestionsListActivity which is as follows after refactoring:
At this point while we have hidden the underlying xml layout and the inflated view hierarchy from the activity, QuestionListActivity still holds a direct reference to QuestionsListMvcViewImpl.
This is a problem because when we later want to introduce a new UI implementation for QuestionsListActivity we have to make changes in all around in all the places where QuestionsListMvcViewImpl is referenced. While this might seem like an easy refactoring in our simple StackOverflow client, in an application with a complex UI and business logic, the refactoring will not be as simple.
What would be ideal is to be able to change the implementation without having to make any changes in QuestionsListActivity.
So inorder to achieve this kind of decoupling between QuestionsListActivity and QuestionsListMvcViewImpl, we will utilize the Dependecy Inversion Principle and define a high level contract class that will act as the intermediary between the two components.
Establishing a high level contract between the View and the Activity
We can define / extract a high level contract from QuestionsListMvcViewImpl which is as follows:
And the implemented by QuestionsListMvcViewImpl:
QuestionListActivity we refactor accordingly as follows:
At this point QuestionsListActivity is still tightly coupled with QuestionsListMvcViewImpl on line 12 when it is instantiated. We can fix this by utilizing the Factory Design Pattern and create a factory that will take care of instantiating QuestionsListMvcViewImpl. This will effectively hide from QuestionsListActivity the details of how QuestionsListMvcView is created thereby completing the abstraction of the View from the Activity.
Creating MvcViewFactory
MvcViewFactory will be responsible for creating implementations the view components. MvcViewFactory is as follows:
Since QuestionsListMvcViewImpl depends on LayoutInflater we inject it into MvcViewFactory via the constructor. Under getQuestionsListMvcView() we instantiate QuestionsListMvcViewImpl which we can then return as QuestionListMvcView since the former derives from the later. The getQuestionsListView() method takes in a parameter parent of type ViewGroup which is the parent of the root view of the view hierarchy, which we pass to the inflate() method of LayoutInflater.
Since MvcViewFactory is injected into our activities, we then create a dependency injection module called ActivityModule that provides MvcViewFactory as follows:
We annotate ActivityModule with @Module so that Hilt will recognize it as providing a dependency. We also annotate it with @InstallIn(ActivityComponent.class) which tells Hilt that ActivityModule is scoped to the lifecycle of Activity. In other words, it ensures that with every new Activity we get a new instance of ActivityModule that is tied to that Activity. This is very important to avoid memory leaks since ActivityModule will hold a reference to the current Activity.
Also since ActivityModule is installed in the ActivityComponent, Hilt will provide references to the current Activity anywhere inside ActivityModule when we need it, in our case in getQuestionListMvcView() method.
After injecting MvcViewFactory, QuestionsListActivity looks as follows:
At this point QuestionsListActivity does not know the implementation details of QuestionsListMvcView, which is in this case QuestionsListMvcViewImpl. As a result we can easily swap between implementations without having to make any changes to QuestionsListActivity, we only have to change the getQuestionListMvcView() method in MvcViewFactory. This also enables us to perform quick A/B testing of user interfaces.
Furthermore, this loose coupling between QuestionsListActivity and QuestionsListMvcViewImpl allows them to be developed independently provided both parties involved are aware of the QuestionsListMvcView contract. For example if we wanted to outsource the development of the user interface we simply give the outsourcing QuestionListMvcView contract class to create an implementation according to our requirements. This way we can rest assured that what will get from the outsourcing will fit seamlessly into the business logic of our application.
You can checkout the code state of the application here.
Creating A Reusable MVC View Class Hierarchy
At this point if we compare QuestionListMvcView and QuestionDetailMvcView and also their respective implementations, they have some similarities, both accidental and fundamental. We can create a reusable class hierarchy that will help us reduce code duplication.
MvcView:
Looking at QuestionListMvcView and QuestionDetailMvcView we can see they both have the getRootView() method. This is a fundamental similarity with respect to being our MvcView components. In other words we will need to get the root view from all our MvcView components. Hence we can create an abstraction interface which is as follows:
There are some operations that we will perform repeatedly in our UI logic such as referencing views, getting Context, getting resources (e.g colors, strings) e.t.c. Likewise, we can create a base implementation of MvcView that will contains some useful convenience methods which is as follows:
The kind of convenience methods to include in BaseMvcView is a matter of personal choice, however it is best to keep them few.
ObservableMvcView:
We have two types of Views in our applications:
- Views that simply display information (in our case QuestionDetailMvcView) and
- views that display information and capture user input (in our case QuestionListMvcView)
In case of the latter we can create an extension of BaseMvcView called BaseObservableMvcView which will allow us to register a listener for user input:
BaseObservableMvcView is generic which enables subclasses to specify different types of listeners through the OBSERVER parameter.
The resulting class hierarchy will look as follows:
In the diagram, QuestionsListMvcView extends BaseObservableMvcView because we want to be able to register observers that will listen for user input when the user clicks a list item. On the other hand, QuestionsDetailMvcView simply displays information so it extends BaseMvcView. Now if we want to create a new MvcView contract we extend either BaseMvcView or BaseObservableMvcView making our job easier.
The state of the code after making these changes can be found here.
Join me in PART 4 where we will see how we can extend this approach to make our code more Unit testable.
** PS — If you need clarification on anything don’t hesitate to reach out in the comment section