A Refined Approach To MVC Architecture in Android Part 2 — Extracting view logic from Activity.
In Part 1, we saw that Activities and Fragments classes are heavily coupled with a lot of android framework classes (chiefly Context) making them naturally suited to act a view controllers in MVC. As a result, to achieve a purer MVC implementation in android, we have to ship all the “view logic” from the Activity / Fragment to a separate component which will act as the View.
In this chapter we will create a view component that take on the responsibility of inflating the xml layout and exposes methods for interacting with the view hierarchy.
The demo app is a simple StackOverflow client which has one screen which displays a list of questions a second that displays the full detail of a selected question.
We will be using the Hilt dependency injection library which makes it super easy to implement dependency injection in Android. For the sake of brevity, I will not go into how Hilt works. If you want to learn about Hilt, here is a codelab and here is a tutorial from the Android documentation.
The Traditional Approach
The following is the xml layout of the Questions list page. It only has a RecyclerView and a ProgressBar.
And then QuestionListActivity implemented using the traditional way might be as follows:
QuestionListActivity implements a basic RecyclerView setup and annotated @AndroidEntryPoint to mark it as a file to be injected by the Hilt library.
GetQuestionsUseCase implements all the logic of fetching and parsing questions data from the StackOverflow api and defines QuestionsListener which is a basic callback interface which QuestionListActivity implements to receive the results delivered by GetQuestionsUseCase. GetQuestionsUseCase ist then injected into QuestionsListActivity by Hilt. Find code here.
GetQuestionsUseCase depends on StackOverflowApi which is provided by StackOverflowApiModule.
GetQuestionsUseCase also extends MyObservable, implements a simple subscriber-observer pattern, and provides two methods to register and unregister listeners. MyObservable is generic, which allows it to accept observers of any type. Find MyObservable code here:
Going back to QuestionListActivity, considering that activity_questions_list.xml is part of the implementation of the view logic, that one line (line 13) immediately leaks the implementation of the question list view logic into our QuestionsListActivity. Furthermore the findViewById() further couples the xml layout with QuestionsListActivity.
When we later receive a xml layout file with a new UI design we not only have to change line 8 but might find ourselves having to change all the calls to findViewById(), removing all the code (like animations) from QuestionListActivity that was only relevant to the UI of the old xml layout file.
Decoupling the xml layout from Activity
In order to decouple activity_questions_list.xml from QuestionsListActivity we need to create a wrapper class, which we will call QuestionsListMvcViewImpl, that will take care of inflating the xml layout and referencing the view hierarchy for us.
After refactoring, the newly created QuestionListMvcViewImpl is as follows:
The constructor takes in an instance of LayoutInflater and ViewGroup. ViewGroup is the the parent of the root that will be inflated.
The getRootMethod() provides a reference to the root view of the layout that Activities and Fragments still need to bind to.
QuestionListMvcViewImpl implements the OnItemClickListener interface defined in QuestionsAdapter in order to listen for when a question list item has been clicked. The implementation in QuestionListMvcViewImpl simply propagates the the call to QuestionsListViewListener’s onQuestionClicked() method.
We then initialize QuestionsListMvcView in QuestionsListActivity as follows:
QuestionsListMvcViewImpl extends the generic MyObservable specifiying QuestionListViewListener as the observer that will listen for user input. QuestionsListActivity implements QuestionListViewListener and then registers under onStart() and deregisters under onStop()
We can already see that QuestionListActivity is looking cleaner that before.
If you use view binding, QuestionsListMvcViewImpl can be as follows:
Questions Detail Screen
And in the same way the Question Detail screen is implemented as follows:
- QuestionDetailMvcViewImpl:
2. QuestionDetailActivity:
The complete state of the code at this point is found here
Now QuestionsListActivity and QuestionDetailActivity do not reference any xml layout files which is an important step in the right direction. However this is not enough to achieve the flexibility that we want because QuestionListActivity and QuestionDetailActivity are coupled to QuestionsListMvcViewImpl and QuestionDetailMvcViewImpl respectively.
This means that if we want to change these view components we have to touch both activities. But what we want is to be able to change the view components on the fly without having to touch the activities.
To achieve this, we need to further abstract away the implementations of QuestionsListMvcViewImpl and QuestionDetailMvcViewImpl by defining their high level contracts in the form of abstract classes and then only expose these contracts to our activities.
We will then create a component called MvcViewFactory, to which we will delegate the responsibility of instantiating implementations of our view components. MvcViewFactory will then be injected into the activities.
This is how the resulting architecture will look like:
In the diagram, each every represents a dependency on a component. For example QuestionsListActivity has a direct dependency on MVCViewFactory. This means that QuestionsListActivity holds a direct reference to MVCViewFactory.
QuestionsListMvcViewImpl is an implementation of QuestionListMvcView, which is an abstract class that acts as the intermediary (or contract) between the two components.
The most important thing to note in the diagram is that there is no arrow directly pointing to QuestionsListMvcViewImpl from QuestionsListActivity. In other words QuestionListActivity does not know about QuestionListMvcViewImpl. This means we can swap QuestionListMvcViewImpl for another implementation of QuestionsListMvcVieew without QuestionListActivity knowing, that is, without making any change to QuestionsistActivity.
Join me in PART 3 where we will see how we can implement in code the architecture in the diagram.