When to use MVC or MVP or MVVM ... or Nothing
Ahmed Adel Ismail
Engineering Manager @ Yassir | x-SadaPay | x-Swvl | x-Talabat | x-TryCarriage | x-Vodafone | More than a decade of experience in Android development and teams leadership
Although We can do any thing with any pattern, but every architecture pattern has it's pros and cons, no one is better than the other, but every one has it's use cases that it can be the best choice when used
If you want to know how to implement those patterns, take a look at this link first :
Before we start
A very common misconception is that ... the Model (the M letter in every pattern) is the objects (POJOs) received from server or database, like User object, or any other data class ... this is not true, the Model stands for a Model layer, a whole layer that consists of Entities layer (where we put all our data classes and POJOs) and the Domain layer (where we put all the classes that holds logic / Business rules, like Repositories and Server-API interfaces, database related classes, etc...) this Model layer is accessed by our Controller or Presenter through a class (or an interface), you can call it Model class, or Inter-actor, or View-Model
So for our examples here, let us call that class as the Model class, where this class is responsible for communicating with the Model layer and holding the state (and responses) for the current screen ... I prefer making this class extend the new architecture components View-Model as it will survive rotation and configuration changes
Using No Architecture Pattern at all
In very rare cases where the screen holds no state or business logic, like for example, if we have a screen that displays couple of Text-Views from local String resources, and a button that opens another screen or an external link, using any pattern will be useless, so
- Use no Architecture pattern if your Screen does not require saving it's state (nothing is updated on this screen after it draws it's views
- Use no Architecture pattern if your screen does not communicate with your Model Layer (like making a server API call, or retrieving something from database or preferences)
MVC - Model View Controller
The major issue with MVC is test-ability, it is hard to test how our actions are reflected on the UI when it is finished, we need to Mock many Android components to be able to test our Controller (Activity/Fragment) but there are some cases when we have screens that communicates with the domain layer (preferences or database or server) without waiting for the result, like for example :
This is our Repository that knows where to save/load data :
public class Repository {
private final SharedPreferences preferences;
public Repository(SharedPreferences preferences) {
this.preferences = preferences;
}
void saveSettingsOne(boolean selected) {
// save to preferences
}
void saveSettingsTwo(boolean selected) {
// save to preferences
}
boolean loadSettingsOne() {
// load from preferences
}
boolean loadSettingsTwo() {
// load from preferences
}
}
And this is our Model class that deals with the Repository :
public class MVCModel extends ViewModel {
private boolean checkBoxOneSelected;
private boolean checkBoxTwoSelected;
private Repository repository;
void initialize(Repository repository) {
this.repository = repository;
}
void loadState() {
checkBoxOneSelected = repository.loadSettingsOne();
checkBoxTwoSelected = repository.loadSettingsOne();
}
void selectCheckBoxOne() {
checkBoxOneSelected = !checkBoxOneSelected;
repository.saveSettingsOne(checkBoxOneSelected);
}
void selectCheckBoxTwo() {
checkBoxTwoSelected = !checkBoxTwoSelected;
repository.saveSettingsTwo(checkBoxTwoSelected);
}
boolean isCheckBoxOneSelected() {
return checkBoxOneSelected;
}
boolean isCheckBoxTwoSelected() {
return checkBoxTwoSelected;
}
}
And this is our Controller (Activity) :
public class MVCActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvc);
MVCModel model = createModel();
initializeCheckboxOne(model);
initializeCheckboxTwo(model);
}
private MVCModel createModel() {
MVCModel model = ViewModelProviders.of(this).get(MVCModel.class);
model.initialize(new Repository(sharedPreferences()));
model.loadState();
return model;
}
private SharedPreferences sharedPreferences() {
return getSharedPreferences("MVC-sample-prefs", MODE_PRIVATE);
}
private void initializeCheckboxOne(final MVCModel model) {
CheckBox checkBoxOne = findViewById(R.id.checkBox);
checkBoxOne.setChecked(model.isCheckBoxOneSelected());
checkBoxOne.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
model.selectCheckBoxOne();
}
});
}
private void initializeCheckboxTwo(final MVCModel model) {
CheckBox checkBoxTwo = findViewById(R.id.checkBox2);
checkBoxTwo.setChecked(model.isCheckBoxTwoSelected());
checkBoxTwo.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
model.selectCheckBoxTwo();
}
});
}
}
In a screen Like this, if we tried to implement in as MVP, we will find that our Presenter will never communicate with the View, since the actions of the Model will never be reflected on the UI ... and even when we are testing this screen, we will not need to Mock our Activity since it holds no logic, and our Model does not depend on The Activity since it does not need to notify it back with any thing, so our Unit test will be similar to this :
public class MVCModelTest {
@Test
public void selectCheckBoxOneThenSwitchTheCheckBoxOneSelectedState() {
MVCModel mvcModel = new MVCModel();
mvcModel.initialize(new MockRepository(false, false));
mvcModel.selectCheckBoxOne();
assertTrue(mvcModel.isCheckBoxOneSelected());
}
}
class MockRepository extends Repository {
private boolean settingsOneState;
private boolean settingsTwoState;
MockRepository(boolean settingsOneState, boolean settingsTwoState) {
super(null);
this.settingsOneState = settingsOneState;
this.settingsTwoState = settingsTwoState;
}
@Override
void saveSettingsOne(boolean selected) {
this.settingsOneState = selected;
}
@Override
void saveSettingsTwo(boolean selected) {
this.settingsTwoState = selected;
}
@Override
boolean loadSettingsOne() {
return settingsOneState;
}
@Override
boolean loadSettingsTwo() {
return settingsTwoState;
}
}
Now Testing is effortless although we are using MVC, summing things up
- We can use MVC when our screen has a One-Direction-Flow of actions, all interactions by the user do affect the Model, but it's result does not affect the UI
- We can use MVC when our screen is simple enough (similar to the no architecture example), but it need to communicate with our Model layer ... and this is why we need to put the man in the middle ... our Model class
MVP - Model View Presenter
When our Screen communicates with our Model, and waits for response to update it's UI, this is where we start to think about more test-able pattern than MVC, which is MVP or MVVM ... but the main point that makes us choose MVP over MVVM is SIMPLICITY ... when we have a SIMPLE screen that holds Bi-Directional-Flow between the UI and the Model, but it updates very limited views when our Model responds with the result from the domain, MVP is the best choice, for example :
This will be our Repository that know how to request a message for our screen :
class Repository {
private final ServerApi serverApi;
Repository(ServerApi serverApi) {
this.serverApi = serverApi;
}
void requestMessage(Consumer<String> onSuccess, Consumer<Exception> onError) {
serverApi.request(onSuccess, onError);
}
}
class ServerApi {
void request(Consumer<String> callback, Consumer<Exception> onError) {
// request from server then return result to callbacks
}
}
And This will be our Model class that deals with Repository :
class MVPModel extends ViewModel {
private Presenter presenter;
private Repository repository;
void initialize(Presenter presenter, Repository repository){
this.presenter = presenter;
this.repository = repository;
}
void requestMessage(){
repository.requestMessage(presenter::onResponseSuccess,presenter::onResponseFailure);
}
}
* presenter::onResponseSuccess is equal to create an anonymous inner class in the method parameter and calling presenter.onResponseSuccess(message) ... this is called method reference
And this is our Presenter interface :
interface Presenter {
void onButtonClicked();
void onResponseSuccess(String responseMessage);
void onResponseFailure(Exception responseError);
}
And this is our View interface :
public interface MVPView {
void switchProgressVisibility(boolean visible);
void updateTextView(String message);
}
And this is our View implementer (Activity) :
public class MVPActivity extends AppCompatActivity implements MVPView {
private ProgressBar progressBar;
private TextView textView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp);
MVPModel model = createModel();
Presenter presenter = createPresenter(model);
initializeModel(presenter, model);
initializeViews(presenter);
}
private MVPModel createModel() {
return ViewModelProviders.of(this).get(MVPModel.class);
}
private PresenterImplementation createPresenter(MVPModel model) {
return new PresenterImplementation(this, model);
}
private void initializeModel(Presenter presenter, MVPModel model) {
ServerApi serverApi = new ServerApi();
Repository repository = new Repository(serverApi);
model.initialize(presenter, repository);
}
private void initializeViews(final Presenter presenter) {
progressBar = findViewById(R.id.progressBar);
textView = findViewById(R.id.textView);
Button button = findViewById(R.id.button);
button.setOnClickListener(view -> presenter.onButtonClicked());
}
@Override
public void switchProgressVisibility(boolean visible) {
progressBar.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
@Override
public void updateTextView(String message) {
textView.setText(message);
}
}
* view -> presenter.onButtonClicked() is the same as creating an OnClickListener and invoking presenter.onButtonClicked() in it ... this is called lambda
And this is the Presenter Implementation :
class PresenterImplementation implements Presenter {
private final WeakReference<MVPView> viewReference;
private final MVPModel model;
PresenterImplementation(MVPView view, MVPModel model) {
this.viewReference = new WeakReference<>(view);
this.model = model;
}
@Override
public void onButtonClicked() {
MVPView view = viewReference.get();
if (view == null) return;
view.switchProgressVisibility(true);
model.requestMessage();
}
@Override
public void onResponseSuccess(String responseMessage) {
MVPView view = viewReference.get();
if (view == null) return;
view.switchProgressVisibility(false);
view.updateTextView(responseMessage);
}
@Override
public void onResponseFailure(Exception responseError) {
MVPView view = viewReference.get();
if (view == null) return;
view.switchProgressVisibility(false);
view.updateTextView(responseError.getMessage());
}
}
* we use WeakReference to hold reference to our View, so when the View is destroyed, our WeakReference will not hold reference to it any more, this protects us from memory leaks
As you see in this example, when the user clicks the button, The presenter handles the click by asking the Model to request a message, and the Presenter waits for the response, when the response is received, the Presenter updates the UI ... if we like to test this behavior in Unit tests, this is easily done by supplying our Mock-View and Mock-Repository and test our Bi-Directional-Flow, for example :
public class PresenterImplementationTest {
@Test
public void onButtonClickThenShowProgress() {
final boolean[] progressShown = {false};
MVPView view = new MVPView() {
@Override
public void switchProgressVisibility(boolean visible) {
if (visible) {
progressShown[0] = true;
}
}
@Override
public void updateTextView(String message) {
}
};
MVPModel model = new MVPModel();
Presenter presenter = new PresenterImplementation(view, model);
model.initialize(presenter, new MockRepository(true));
presenter.onButtonClicked();
assertTrue(progressShown[0]);
}
@Test
public void onButtonClickThenUpdateTextViewOnResponse() {
final boolean[] textUpdated = {false};
MVPView view = new MVPView() {
@Override
public void switchProgressVisibility(boolean visible) {
}
@Override
public void updateTextView(String message) {
textUpdated[0] = true;
}
};
MVPModel model = new MVPModel();
Presenter presenter = new PresenterImplementation(view, model);
model.initialize(presenter, new MockRepository(true));
presenter.onButtonClicked();
assertTrue(textUpdated[0]);
}
}
class MockRepository extends Repository {
private final boolean success;
MockRepository(boolean success) {
super(null);
this.success = success;
}
@Override
void requestMessage(Consumer<String> onSuccess, Consumer<Exception> onError) {
if (success) {
onSuccess.accept("SUCCESS MESSAGE");
} else {
onError.accept(new UnsupportedOperationException());
}
}
}
And now we can test the user interaction and it's effect when it's response is received from the Model layer ... summing things up :
- We can use MVP when our screen has Bi-Directional-Flow, where user interactions need to request something from our Model layer, and the result of this request will affect the UI
- We can use MVP when the UI elements affected by the updates from Model Layer are very limited
- It is bad Idea to use MVP when the UI is updated without user inter-actions, like updating UI when an event happens in the Model Layer, this approach is closer to MVVM more than MVP ... more on this point when we come to MVVM
MVVM - Model View View-Model
Imagine in our MVP example that our presenter will update too many views when the message is received, like for example :
@Override
public void onResponseSuccess(String responseMessage) {
MVPView view = viewReference.get();
if (view == null) return;
view.switchProgressVisibility(false);
view.updateTextView(responseMessage);
view.enableButton();
view.animateFloatingActionButton();
view.showRefreshButton();
view.displayToastMessage(responseMessage);
...
}
now our View is not SIMPLE any more, we have too much Views that will be affected by the updates done from our Model Layer, and handling all those views on different requests from Model Layer will be very hard to maintain ... now we can detect that it is time to shift for MVVM
- We can use MVVM when our Screen holds many views, at this point it is easier to make each view subscribe on it's data-source in the View-Model and handle it-self when this data-source changes ... this data-source can be Live-Data or RxJava2 Observable, or what ever framework used
- We can use MVVM also when our screen has a One-Directional-Flow, this time the events are coming from the Model Layer and affecting the UI without any user interactions ... and this is the exact opposite for MVC ... you can detect this when you work with MVP and find that your presenter is just updating the UI but not requesting any thing from the Model ... a common scenario about this case is a screen that updates your location on Map, so when-ever the location changes, the map is updated, so we can make our Views in the Activity/Fragment observe on the variable that holds the location in the View-Model, and this variable is updated every time the location is changed
One of the limitations to use MVVM is the learning curve for the framework to be used, you can either implement it by Rx-Java2 or Live-Data or Android-Binding or Rx-Binding, or maybe all of them together, It holds learning curve which makes it harder than other Architecture patterns
Although those are the most popular architecture patterns those days, there are other Architecture patterns available, like VIPER, MVI, MVVM-C, REDUX, FLUX, and many others.
I recommend watching the awesome video by Hannes Dorfmann :
Software Engineer
4 年Thank you, it helped me understand the difference between these patterns more clearly :)
Senior Mobile Software Engineer (Android - Flutter - IOS ) ??
5 年Ahmed Adel Ismail firstly thanks alot for this article? but since most app i work have lots of screens and data?I think MVVM is the best Way to struct code for for some reasons: - UI components are kept away from business logic. -business login is kept away from database operations -easy to understand and read. -a lot less to worry with it come to managing life cycle events- when user close app for some hours or rotation app? -the app will be in the same position and state when user left. life cycle state of App will be maintained? i like to listen your opinion about that?
Senior Engineer III at Verve Group Permanent Resident at Deutschland
5 年Thanks a lot for this great article ,i really now have the feature to know which pattern to be used instead of having static architecture to be used every time Thank you Ahmed :)
Senior Android Developer | Jetpack Compose | Kotlin multiplatform | Flutter
6 年Hani AlMomani
Staff iOS Engineer | Building Scalable Mobile Apps | Mobile Lead | Mentor
6 年I read this article many times, Every time I learn something new, Thank you for this effort :) But I have a question? in the MVP? Example you passed the model to the presenter in the View "" Isn't the view know nothing about the Model, in other words the model should be encapsulated into the presenter?