A Refined Approach to MVC Architecture in Android Part 4 — Creating a standalone unit-testable controller.
In Part 3 we completed the decoupling of the Activities from the implementation details of our views in our StackOverflow client application which enables easy swapping between implementation of view components for the screens of the application with minimal effort.
At this point all the business logic of our application is in the Activities. If you do not have unit testing in mind we can leave the state as it is. But when you are building a not trivial application, you want to make your code as unit testable as possible.
The problem with Activities is that they are not unit testable, or at least unit testable using conventional unit testing tools like JUnit testing framework. As a result all the code we directly put inside these components is also not unit testable. To work around this problem, we can extract all the code from the Activity and put it into a standalone controller class.
Extracting Unit Testable Controllers
Looking at our StackOverflow client application the current state of QuestionsListActivity is as follows:
We can extract all the business code and put it in a new class called QuestionsListController as follows:
QuestionsListController is essentially the same in structure as QuestionsListActivity and the only major difference is that GetQuestionsUseCase is now being injected by Hilt via its constructor.
This leaves us with two problems under onQuestionsFetchError() when we show the toast message and under onQuestionClicked() when we navigate to QuestionDetailActivity. In these two methods Android framework code is leaked into the controller making it also not unit testable.
To fix this we will create two components, ToastManager and ActivityNavigator, to which we will delegate the responsibility of showing toast messages and handling navigation respectively.
ToastManager is as follows:
ToastManager will handle showing all the toast messages in our application. For example if we later add the ability to answer questions in QuestionDetailsActivity, we can add a method like showAnswerPostedSuccessfullyMessage() to show a toast message when that operation is successful.
MyNavigator is as follows:
Likewise MyNavigator will handle all the navigation between the activities of our application.
We then inject these two components into QuestionsListController via the constructor and make the corresponding refactoring:
With these changes made our controller becomes unit testable because it no longer references any android framework code. During testing MyNavigator and ToastManager can be easily mocked out with mocking frameworks such as Mockito.
One more step is required to make QuestionsListController work — we need to tell Hilt how to provide MyNavigator and ToastManager. Now to avoid memory leaks, MyNavigator and ToastManager need to be scoped to the Activity. To achieve this we expose methods in ActivityModule which provide these two components as follows:
Note that we annotate the parameter of providesToastManager() with @ActivityContext to tell Hilt to provide the Context of the current Activity.
We then make the corresponding changes in QuestionListActivity to take into account QuestionListController as follows:
At this point QuestionsListActivity is now only a shell with all the business logic inside the unit-testable QuestionsListController. During testing, we can easily supply QuestionsListController with mock version of it dependencies. For example we can use Mockito to create a mock version of QuestionListMvcView inorder to test the integration between the two components.
The less our code is tightly coupled with the android framework, the more easily unit testable it becomes.
On another note, the advantage of creating components like ToastManager and MyNavigator, which encapsulate implementation details of common functions within our application, is that they provide a way to centrally manage these functions. For example if we later decide to show messages in our application using a Snackbar, we can simply refactor ToastManager as follows:
And now in both QuestionsListActivity and QuestionsDetailActivity messages will show as Snackbar. Likewise we can centrally manage navigation properties like transitions inside MyNavigator. This kind of flexibility is dispensable when our applications increase in complexity.
The code state of the application at this point is found here.
Join me in the next and final chapter of this article where we will see how this approach is easily extensible to Fragments and RecyclerView lists.