This is the second article of the series on android architecture. The first part outlines the content to be discussed in the series.
What is MVC architecture?
The model-view-controller (MVC) has been the traditional architectural pattern for developing applications having user interface. Even though as an android architecture for developing apps, it is not much popular in android communities, it gave rise to the widely-used MVP and MVVM architectures, as Martin Fowler explains here.
MVC in simple words
In MVC, the View layer only knows how to show the user interface (UI). If any data is needed for this, it gets it from the Model layer. But the View does NOT directly ask the model to find the data, it does it through the Controller. So the Controller calls the Model to provide the required data for the View. Once the data is ready, the Controller informs the View that the data is ready to be acquired from the Model. Now the View can get the data from the Model.
This flow can be summarised as below:
It is worth noting that the View can know about the availability of the data in the Model either through Controller — also known as Passive MVC — or by observing the data in the Model by registering observables to it, which is Active MVC.
MVC architecture in android
For better understanding of the described flow, let’s apply it to the movie example app introduced in the previous part, which lists the movie titles containing a keyword entered by the user after the press of a button.
The complete example can be found in the ‘mvc’ branch of my GitHub repo here.
On the implementation part, one of the first things that comes to mind is that which android component should be used for the View? Activity or Fragment ?
The answer is that it does not matter and both can be used. The View should be able to present the user interface (UI) on the device and respond to the user’s interaction with the UI. Both Activity and Fragment provide the required methods for this.
In this example I have used Activity for the View layer, but Fragment can also be used.
When the ‘FIND’ button is clicked, the View ( MainActivity.class) informs the Controller ( MainController.class) by calling its findAddress method:
1 |
main_activity_button.setOnClickListener({ mMainController.findAddress(main_activity_editText.text.toString()) }) |
The Controller ( MainController.class) then does two things: it calls the Model ( MainModel.class) to search for the list of movies containing the keyword. At the same time, it notifies the View ( MainActivity.class) that the data is now loading, for it to show the progress bar:
1 2 3 4 |
fun findAddress(address: String) { mainView.showProgressBar() mainModel.findAddress(address) } |
Now the two-way relation between the View and Controller layers becomes clear.
The Model ( MainModel.class) then calls the relevant search service and (in the Passive MVC employed here) informs the Controller ( MainController.class) whether the search result is ready (response successful) or there is an error (response unsuccessful):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fun findAddress(address: String) { val disposable: Disposable = fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<ResultEntity>?>() { override fun onSuccess(t: List<ResultEntity>) { mList = t controller.doWhenResultIsReady() } override fun onError(e: Throwable) { httpException = e as HttpException controller.doWhenThereIsErrorFetchingTheResult() } }) compositeDisposable.add(disposable) } |
In the Controller ( MainController.class) then the appropriate methods from the View ( MainActivity.class) are called to reflect on the response :
1 2 3 4 5 6 7 8 9 |
fun doWhenResultIsReady() { mainView.hideProgressBar() mainView.showResult() } fun doWhenThereIsErrorFetchingTheResult() { mainView.hideProgressBar() mainView.showError() } |
Finally, the View ( MainActivity.class) either shows the search results by getting them directly from the Model, or shows an error message:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fun showResult() { updateMovieList(mMainModel.mList) } fun showError() { showToast(this, getString(R.string.error_getting_results, mMainModel.httpException.message)) } fun showProgressBar() { main_activity_progress_bar.visibility = View.VISIBLE } fun hideProgressBar() { main_activity_progress_bar.visibility = View.GONE } |
What I like and dislike about MVC in android?
Compared to the ‘no architecture’ case in the previous post, MVC renders testability, maintainability and scalability. It also satisfies Single Responsibility Principle of SOLID… so what are its points of weakness that I don’t like much?
1- View depends both on Controller and Model
This means there is not a single source of updating the View. In fact, if updating the UI requires data (e.g. from network or database), the View gets it from the Model. If not (e.g. hiding or showing the progress bar or going to another activity), the View gets the updates from the Controller.
Therefore, for unit testing the View, both Controller and Model must be mocked ( MainActivityTest.class).
2- Model is doing too much work
Let’s take a look at the Model for the movie example, i.e. MainModel.class.
The method findAddress is called by the Controller to get the movie search results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fun findAddress(address: String) { val disposable: Disposable = fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<ResultEntity>?>() { override fun onSuccess(t: List<ResultEntity>) { mList = t controller.doWhenResultIsReady() } override fun onError(e: Throwable) { httpException = e as HttpException controller.doWhenThereIsErrorFetchingTheResult() } }) compositeDisposable.add(disposable) } |
In the above method, the Model:
- makes the connection to the data server to get the result
- if the data is ready, it notifies the Controller through controller.doWhenResultIsReady()
- if there is error getting the data, it notifies the Controller through controller.doWhenThereIsErrorFetchingTheResult()
When the connection needs to be cancelled (when onStop() method of the MainActivity.class is called) again the Model takes care of it:
1 2 3 |
fun stopLoadingTheList() { compositeDisposable.clear() } |
Further, let’s suppose we want to act differently upon the HTTP errors we get from the server, e.g. we want to show a resource-not-found page corresponding to 404 error and a not-authorised page corresponding to 403 error. For this, we should modify the onError method above to call different methods from the Controller.
Isn’t that too much?
Yes, Model does too many things here, that intuitively should not.
In other words, instead of the Model telling us what to do, we would rather observe the Model and decide what to do based on its result elsewhere.
3- Who controls the UI logic?
The app currently shows the list of movie search results in the format of “${Year}: ${Movie Name}” (this screenshot).
Question: Who is controlling this logic?
Answer: This method from the View ( MainActivity.class ):
1 2 3 |
fun fetchItemText(it: MainModel.ResultEntity): String { return "${it.year}: ${it.title}" } |
Question: Does that mean the View decides how to show the result? That is if we want to change the formatting to “${Movie Name}: ${Year}” we should consult the View?
Answer: Yes
Question: I thought the View should not handle the logic…, what if we get the formatting from elsewhere?
Answer: You mean the Model? Because we are not getting any data from the Controller.
Question: Well… then yes, what is the problem with that?
Answer: The Model is already doing too much work, do you want it to handle the UI logic as well?
Question: So, … what can we do?
Answer: Nothing, this is another reason I am not a fan of MVC 😀
Compared to the “no architecture” case in the previous part, the MVC architecture provides many improvements by bringing testability, maintainability and scalability on the table as well as meeting the SOLID principles.
However, there are some drawbacks that stem from the fact that the Controller does not pass the any data DIRECTLY to the View to show. It just notifies the View to get it directly from the Model. In this way, either View or Model need to do too many tasks we don’t expect them to do.
The MVP architecture in the next part resolves the above shortcomings, so stay tuned 😉