When to use MVC or MVP or MVVM ... or Nothing

When to use MVC or MVP or MVVM ... or Nothing

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 :





Thank you, it helped me understand the difference between these patterns more clearly :)

回复
Ahmed Tawfiq

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?

Abanoub Barayo

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 :)

回复
Omar Altamimi

Senior Android Developer | Jetpack Compose | Kotlin multiplatform | Flutter

6 年
回复
Abozaid Ibrahim

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?

回复

要查看或添加评论,请登录

Ahmed Adel Ismail的更多文章

  • Sharing data across multiple Mobile Squads - with examples

    Sharing data across multiple Mobile Squads - with examples

    Earlier I shared an article suggesting a solution to a common problem with teams following the "Spotify Model", which…

  • SDD - Squad Driven Design

    SDD - Squad Driven Design

    Working in multiple big teams I've found that we are always trying to apply our known best practices in software, but…

    4 条评论
  • Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Before we start, this article requires basic knowledge about the following topics : Clean Architecture Unit Testing…

    10 条评论
  • Android - A Cleaner Clean Architecture

    Android - A Cleaner Clean Architecture

    It has been a while now since Clean Architecture was out, and even many of us started embracing hexagonal (ports and…

    10 条评论
  • Beyond Functional Programming

    Beyond Functional Programming

    In the Android industry, lately functional programming was the all new stuff to learn, RxJava, Kotlin, and the whole…

    7 条评论
  • Dependency Injection in Clean Architecture

    Dependency Injection in Clean Architecture

    After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers…

    18 条评论
  • Meta Programming in Android

    Meta Programming in Android

    Year after year we are getting rid of the boilerplate code that we need to write for small and simple tasks in Android,…

    2 条评论
  • MVI Pattern For Android In 4 Steps

    MVI Pattern For Android In 4 Steps

    Lately I wrote an article about MVI pattern, but as we are facing new problems every day and face more use-cases, we…

    7 条评论
  • Agile - Moving Fast

    Agile - Moving Fast

    We always here about Agile, and think about which methodology do we use, what practices do we have, team velocity…

    1 条评论
  • Kotlin Unit Testing with Mockito

    Kotlin Unit Testing with Mockito

    I've always believed that, if the code is designed to be tested, we wont need any testing framework or library ..

    17 条评论

社区洞察

其他会员也浏览了