This article is sponsored by Yu Gang Said Writing Platform

Author: Zackratos

Copyright notice: The copyright of this article belongs to the wechat public account Yu Gangshuo

What is the MVP

MVP full name: Model-view-Presenter; MVP evolved from the classic MVC pattern. Their basic ideas are similar: Controller/Presenter handles logic processing, Model provides data, and View displays.

Why MVP

Before we discuss why we should use the MVP architecture, we need to understand the characteristics and disadvantages of the traditional MVC architecture.

First take a look at the model diagram of the MVC architecture, as follows

This diagram is very simple. When the View needs to be updated, it first goes to the Controller, and then the Controller goes to the Model to get the data. After the Model gets the data, it directly updates the View.

In MVC, the View has direct access to the Model. As a result, the View contains Model information and, inevitably, some business logic. In the MVC Model, it pays more attention to the invariability of the Model, while there are multiple different displays of the Model at the same time, namely the View. So, in an MVC Model, the Model doesn’t depend on the View, but the View does depend on the Model. Moreover, it is difficult to change the View because some business logic is implemented in the View, or at least that business logic is not reusable.

Editor’s note: In most cases, the View and Model do not interact directly, but indirectly through the Controller.

This may sound a little abstract, but let’s use a simple example to illustrate.

Suppose you have a requirement that your Activity has a Button and a TextView. When you click on the Button, you ask the network to fetch a string and display the string in the TextView

public class MVCActivity extends AppCompatActivity {

    private Button button;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        button = findViewById(R.id.button);
        textView = findViewById(R.id.text_view);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                newHttpModel(textView).request(); }}); }}public class HttpModel {
    private TextView textView;

    public HttpModel(TextView textView) {
        this.textView = textView;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg); textView.setText((String) msg.obj); }};public void request(a) {
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                try {
                    Thread.sleep(2000);
                    Message msg = handler.obtainMessage();
                    msg.obj = "Data from the network.";
                    handler.sendMessage(msg);
                } catch(InterruptedException e) { e.printStackTrace(); } } }).start(); }}Copy the code

The code is very simple. When clicking on a Button, create an HttpModel object and pass in the TextView object as an argument. Then call its Request method to request data. The flow fits perfectly with the MVC architecture diagram above.

But there’s a problem here. First of all, obviously, HttpModel is the Model layer, so what about the View layer and the Controller layer, so let’s analyze what the View layer and the Model layer are doing, and in this case, What the View layer does is basically update the TextView when it gets the network data, and what the Controller layer does is basically create the HttpModel object and call its Request method, We found that MVCActivity acts as both the View layer and the Controller layer.

This will cause two problems. First, the View layer and the Controller layer are not separated, so the logic is chaotic. Second, due to the coupling of View and Controller layer, activities or fragments are bloated and have a large amount of code. Because of the simplicity of this example, neither of these problems is obvious, and if there is a large volume of business in the Activity, the problem will manifest itself and the development and maintenance costs will be high.

How to Use MVP

If MVC has these problems, how can it be improved? The answer is to use the ARCHITECTURE of MVP. The definition of MVP architecture has been described earlier

When a View needs to update data, it goes to the Presenter, and the Presenter goes to the Model and asks for data. When the Model gets the data, it notifies the Presenter, and the Presenter notifies the View to update the data. Instead of interacting directly with the Model and View, all interaction is done by the Presenter, who acts as a bridge. Obviously, Presenter must hold a reference to both View and Model objects in order to communicate between them.

Next modify the above example with the MVP architecture as follows

interface MVPView {
    void updateTv(String text);
}


public class MVPActivity extends AppCompatActivity implements MVPView {
    private Button button;
    private TextView textView;
    private Presenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        button = findViewById(R.id.button);
        textView = findViewById(R.id.text_view);
        presenter = new Presenter(this);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { presenter.request(); }}); }@Override
    public void updateTv(String text) { textView.setText(text); }}interface Callback {
    void onResult(String text);
}


public class HttpModel {
    private Callback callback;

    public HttpModel(Callback callback) {
        this.callback = callback;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg); callback.onResult((String) msg.obj); }};public void request(a) {
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                try {
                    Thread.sleep(2000);
                    Message msg = handler.obtainMessage();
                    msg.obj = "Data from the network.";
                    handler.sendMessage(msg);
                } catch(InterruptedException e) { e.printStackTrace(); } } }).start(); }}public class Presenter {
    private MVPView view;
    private HttpModel model;

    public Presenter(MVPView view) {
        this.view = view;
        model = new HttpModel(new Callback() {
            @Override
            public void onResult(String text) {
                Presenter.this.view.updateTv(text); }}); }public void request(a) { model.request(); }}Copy the code

To briefly explain the above code, first create an MVPView interface, which is the View layer, with a method to update the TextView. Then let the Activity implement this interface and duplicate the method to update the TextView. The Model layer no longer passes in a TextView. Instead, it passes in a Callback interface, called Callback, because network requests for data are asynchronous and require Callback to notify Presenter when data has been retrieved. Presenter is also very simple. It first holds a reference to both View and Model in its constructor and then provides a request method externally.

The Presenter calls the Request method when the Button is clicked. The Presenter calls the Request method inside the Button. When the request method is called, the Presenter switches to the main thread. Call the onResult method of the callback to notify the Presenter, and the Presenter will call the View’s updateTv method to update the TextView. The View and Model do not interact directly with each other. All interaction takes place in the Presenter.

Matters needing attention

An MVPView interface for an Activity that is simply passed to a Presenter is not an interface. This is certainly possible. The main purpose of using an interface is to reuse the code. Imagine that if an Activity is passed in directly, then the Presenter can only serve that Activity. For example, suppose that an App has been developed and can be used normally on mobile phones, but now it needs to be adapted on tablets. The interface display effect on tablets has changed, and TextView is not directly in the Activity, but in the Fragment. If you don’t use the View interface, you’ll need to write another Presenter for fragments and do the whole process again. The View interface, however, is easy to use. The Fragment implements the View interface and then overwrites the methods in the View. The Presenter and Model layer do not need to make any changes. Similarly, the Model layer can be written as an interface.

Preventing memory leaks The above code is at risk of memory leaks. If the Model exits the Activity after the Button is clicked but before the Model retrieves the data, the Activity is referenced by the Presenter and the Presenter is doing a time-consuming operation, so the Activity object cannot be recycled. The Presenter’s reference to the View is null when the Activity exits.

// Presenter.java
public void detachView(a) {
    view = null;
}

// MVPActivity.java
@Override
protected void onDestroy(a) {
    super.onDestroy();
    presenter.detachView();
}
Copy the code

Another problem is that while the Activity does not leak memory, it does not make sense to request data in the Model after the Activity exits, so you should also cancel the Handler task in the detachView method to avoid wasting resources. This one is easier, so I won’t post the code.

The MVP of the encapsulation

Obviously, the MVP implementation is roughly the same. If there are a large number of activities and fragments in an application, and all of them use the MVP architecture, there will inevitably be a lot of repetitive work, so encapsulation is very necessary.

Before I talk about MVP encapsulation, it is important to note that MVP is more of an idea than a model, and each developer can implement a personalized MVP in his or her own way. Therefore, there may be some differences in the MVP written by different people.

First, Model, View, and Presenter are all likely to have some generic operations, so you can define three corresponding low-level interfaces.

interface BaseModel {}interface BaseView {
    void showError(String msg);
}

public abstract class BasePresenter<V extends BaseView.M extends BaseModel> {
    protected V view;
    protected M model;

    public BasePresenter(a) {
        model = createModel();
    }

    void attachView(V view) {
        this.view = view;
    }

    void detachView(a) {
        this.view = null;
    }

    abstract M createModel(a);
}
Copy the code

Here, the View layer adds a general method to display error messages, written in the interface layer, which can be displayed at the implementation site according to requirements. For example, some places may pop up a Toast, or some places need to display error messages in the TextView. The Model layer can also add a general method according to requirements. Focus on the Presenter layer.

Here BasePresenter uses generics, so why? Presenters must hold a reference to both View and Model, but there is no way to determine their type in the underlying interface, only that they are subclasses of BaseView and BaseModel. In subclasses of BasePresenter, once the View and Model types are defined, their objects are automatically referenced. The main methods used in Presenter are attachView and detachView, which are used to create a View object and empty the View object, respectively. As mentioned earlier, this is to prevent memory leaks. An object for Model can be created in the Presenter constructor. In addition, presenters can also be written as interfaces, and readers can choose according to their preferences.

Then take a look at how to use MVP encapsulation in business code, as follows

interface TestContract {

    interface Model extends BaseModel {
        void getData1(Callback1 callback1);
        void getData2(Callback2 callback2);
        void getData3(Callback3 callback3);
    }

    interface View extends BaseView {
        void updateUI1(a);
        void updateUI2(a);
        void updateUI3(a);
    }

    abstract class Presenter extends BasePresenter<View.Model> {
        abstract void request1(a);
        abstract void request2(a);
        void request3(a) {
            model.getData3(new Callback3() {
                @Override
                public void onResult(String text) { view.updateUI3(); }}); }}}Copy the code

Define a Contract interface, and then add the subclasses of Model, View, and Presenter to the inner part of a Contract, where a Contract corresponds to a page (an Activity or Fragment). The purpose of this Contract is to keep logical methods on the same page together for easy viewing and modification. The Request3 method in Presenter demonstrates how a View and Model can interact with a Presenter.

The next thing to do is to implement the logical methods of these three modules. Implement the interface of TextContract.View in the Activity or Fragment. Create two classes to implement textContract. Model and TextContract.Presenter, and duplicate the abstract methods in them.

Extensions: Simplify code with RxJava

In the code above, each method in the Model layer passes in a callback interface. This is because retrieving data is often asynchronous and requires the callback interface to notify Presenter to update the View when retrieving data.

If you want to avoid the callback interface, you can use RxJava to retrieve data from the Model and return it directly to an Observable. Then you can use RxJava to modify the previous example

public class HttpModel {
    public Observable<String> request(a) {
        return Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                Thread.sleep(2000);
                emitter.onNext("Data from the network."); emitter.onComplete(); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); }}public class Presenter {
    private MVPView view;
    private HttpModel model;

    public Presenter(MVPView view) {
        this.view = view;
        model = new HttpModel();
    }

    private Disposable disposable;

    public void request(a) {
        disposable = model.request()
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String s) throws Exception { view.updateTv(s); }},new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {}}); }public void detachView(a) {
        view = null;
        if(disposable ! =null&&! disposable.isDisposed()) { disposable.dispose(); }}}Copy the code

The Model’s request method returns an Observable and calls the Subscribe method in the Presenter to notify the View of updates, avoiding the need for callback interfaces.

Open Source Library Recommendations

Finally, I recommend an open source library for MVP architecture. As I said, MVP is more of an idea, so there are not many open source libraries for MVP on Github. Most of them are self-encapsulated MVPS inside the complete APP. If you want an easy architecture to integrate with MVP, I recommend this library: github.com/sockeqwe/mo… Its use method is relatively simple, you can refer to the official demo directly, then simple analysis of the author’s encapsulation idea.

First, the View layer and Presenter layer each have a basic interface

public interface MvpView {}public interface MvpPresenter<V extends MvpView> {

  /** * Set or attach the view to this presenter */
  @UiThread
  void attachView(V view);

  /** * Will be called if the view has been destroyed. Typically this method will be invoked from * Activity.detachView() or Fragment.onDestroyView() */
  @UiThread
  void detachView(boolean retainInstance);
}
Copy the code

The @uithRead annotation is added to ensure that attachView and detachView are running on the main thread. Then the Activity of the business code needs to inherit from MvpActivity

public abstract class MvpActivity<V extends MvpView.P extends MvpPresenter<V>>
    extends AppCompatActivity implements MvpView.com.hannesdorfmann.mosby3.mvp.delegate.MvpDelegateCallback<V.P> {

  protected ActivityMvpDelegate mvpDelegate;
  protected P presenter;
  protected boolean retainInstance;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getMvpDelegate().onCreate(savedInstanceState);
  }

  @Override protected void onDestroy(a) {
    super.onDestroy();
    getMvpDelegate().onDestroy();
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    getMvpDelegate().onSaveInstanceState(outState);
  }

  @Override protected void onPause(a) {
    super.onPause();
    getMvpDelegate().onPause();
  }

  @Override protected void onResume(a) {
    super.onResume();
    getMvpDelegate().onResume();
  }

  @Override protected void onStart(a) {
    super.onStart();
    getMvpDelegate().onStart();
  }

  @Override protected void onStop(a) {
    super.onStop();
    getMvpDelegate().onStop();
  }

  @Override protected void onRestart(a) {
    super.onRestart();
    getMvpDelegate().onRestart();
  }

  @Override public void onContentChanged(a) {
    super.onContentChanged();
    getMvpDelegate().onContentChanged();
  }

  @Override protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    getMvpDelegate().onPostCreate(savedInstanceState);
  }

  /**
   * Instantiate a presenter instance
   *
   * @return The {@link MvpPresenter} for this view
   */
  @NonNull public abstract P createPresenter(a);

  /**
   * Get the mvp delegate. This is internally used for creating presenter, attaching and detaching
   * view from presenter.
   *
   * <p><b>Please note that only one instance of mvp delegate should be used per Activity
   * instance</b>.
   * </p>
   *
   * <p>
   * Only override this method if you really know what you are doing.
   * </p>
   *
   * @return {@link ActivityMvpDelegateImpl}
   */
  @NonNull protected ActivityMvpDelegate<V, P> getMvpDelegate(a) {
    if (mvpDelegate == null) {
      mvpDelegate = new ActivityMvpDelegateImpl(this.this.true);
    }

    return mvpDelegate;
  }

  @NonNull @Override public P getPresenter(a) {
    return presenter;
  }

  @Override public void setPresenter(@NonNull P presenter) {
    this.presenter = presenter;
  }

  @NonNull @Override public V getMvpView(a) {
    return (V) this; }}Copy the code

MvpActivity holds an ActivityMvpDelegate object that implements ActivityMvpDelegateImpl and needs to be passed to the MvpDelegateCallback interface, The code for ActivityMvpDelegateImpl is as follows

public class ActivityMvpDelegateImpl<V extends MvpView.P extends MvpPresenter<V>>
    implements ActivityMvpDelegate {

  protected static final String KEY_MOSBY_VIEW_ID = "com.hannesdorfmann.mosby3.activity.mvp.id";

  public static boolean DEBUG = false;
  private static final String DEBUG_TAG = "ActivityMvpDelegateImpl";

  private MvpDelegateCallback<V, P> delegateCallback;
  protected boolean keepPresenterInstance;
  protected Activity activity;
  protected String mosbyViewId = null;

  / * * *@param activity The Activity
   * @param delegateCallback The callback
   * @param keepPresenterInstance true, if the presenter instance should be kept across screen
   * orientation changes. Otherwise false.
   */
  public ActivityMvpDelegateImpl(@NonNull Activity activity,
      @NonNull MvpDelegateCallback<V, P> delegateCallback, boolean keepPresenterInstance) {

    if (activity == null) {
      throw new NullPointerException("Activity is null!");
    }

    if (delegateCallback == null) {
      throw new NullPointerException("MvpDelegateCallback is null!");
    }
    this.delegateCallback = delegateCallback;
    this.activity = activity;
    this.keepPresenterInstance = keepPresenterInstance;
  }

  /**
   * Determines whether or not a Presenter Instance should be kept
   *
   * @param keepPresenterInstance true, if the delegate has enabled keep
   */
  static boolean retainPresenterInstance(boolean keepPresenterInstance, Activity activity) {
    returnkeepPresenterInstance && (activity.isChangingConfigurations() || ! activity.isFinishing()); }/**
   * Generates the unique (mosby internal) view id and calls {@link
   * MvpDelegateCallback#createPresenter()}
   * to create a new presenter instance
   *
   * @return The new created presenter instance
   */
  private P createViewIdAndCreatePresenter(a) {

    P presenter = delegateCallback.createPresenter();
    if (presenter == null) {
      throw new NullPointerException(
          "Presenter returned from createPresenter() is null. Activity is " + activity);
    }
    if (keepPresenterInstance) {
      mosbyViewId = UUID.randomUUID().toString();
      PresenterManager.putPresenter(activity, mosbyViewId, presenter);
    }
    return presenter;
  }

  @Override public void onCreate(Bundle bundle) {

    P presenter = null;

    if(bundle ! =null && keepPresenterInstance) {

      mosbyViewId = bundle.getString(KEY_MOSBY_VIEW_ID);

      if (DEBUG) {
        Log.d(DEBUG_TAG,
            "MosbyView ID = " + mosbyViewId + " for MvpView: " + delegateCallback.getMvpView());
      }

      if(mosbyViewId ! =null&& (presenter = PresenterManager.getPresenter(activity, mosbyViewId)) ! =null) {
        //
        // Presenter restored from cache
        //
        if (DEBUG) {
          Log.d(DEBUG_TAG,
              "Reused presenter " + presenter + " for view "+ delegateCallback.getMvpView()); }}else {
        //
        // No presenter found in cache, most likely caused by process death
        //
        presenter = createViewIdAndCreatePresenter();
        if (DEBUG) {
          Log.d(DEBUG_TAG, "No presenter found although view Id was here: "
              + mosbyViewId
              + ". Most likely this was caused by a process death. New Presenter created"
              + presenter
              + " for view "+ getMvpView()); }}}else {
      //
      // Activity starting first time, so create a new presenter
      //
      presenter = createViewIdAndCreatePresenter();
      if (DEBUG) {
        Log.d(DEBUG_TAG, "New presenter " + presenter + " for view "+ getMvpView()); }}if (presenter == null) {
      throw new IllegalStateException(
          "Oops, Presenter is null. This seems to be a Mosby internal bug. Please report this issue here: https://github.com/sockeqwe/mosby/issues");
    }

    delegateCallback.setPresenter(presenter);
    getPresenter().attachView(getMvpView());

    if (DEBUG) {
      Log.d(DEBUG_TAG, "View" + getMvpView() + " attached to Presenter "+ presenter); }}private P getPresenter(a) {
    P presenter = delegateCallback.getPresenter();
    if (presenter == null) {
      throw new NullPointerException("Presenter returned from getPresenter() is null");
    }
    return presenter;
  }

  private V getMvpView(a) {
    V view = delegateCallback.getMvpView();
    if (view == null) {
      throw new NullPointerException("View returned from getMvpView() is null");
    }
    return view;
  }

  @Override public void onDestroy(a) {
    boolean retainPresenterInstance = retainPresenterInstance(keepPresenterInstance, activity);
    getPresenter().detachView(retainPresenterInstance);
    if(! retainPresenterInstance && mosbyViewId ! =null) {
      PresenterManager.remove(activity, mosbyViewId);
    }

    if (DEBUG) {
      if (retainPresenterInstance) {
        Log.d(DEBUG_TAG, "View"
            + getMvpView()
            + " destroyed temporarily. View detached from presenter "
            + getPresenter());
      } else {
        Log.d(DEBUG_TAG, "View"
            + getMvpView()
            + " destroyed permanently. View detached permanently from presenter "+ getPresenter()); }}}@Override public void onPause(a) {}@Override public void onResume(a) {}@Override public void onStart(a) {}@Override public void onStop(a) {}@Override public void onRestart(a) {}@Override public void onContentChanged(a) {}@Override public void onSaveInstanceState(Bundle outState) {
    if(keepPresenterInstance && outState ! =null) {
      outState.putString(KEY_MOSBY_VIEW_ID, mosbyViewId);
      if (DEBUG) {
        Log.d(DEBUG_TAG,
            "Saving MosbyViewId into Bundle. ViewId: " + mosbyViewId + " for view "+ getMvpView()); }}}@Override public void onPostCreate(Bundle savedInstanceState) {}}Copy the code

The code is a little long, but the logic is pretty clear. What it does is create Presenter objects in the onCreate method depending on the situation, It is stored in MvpDelegateCallback via the setPresenter method of MvpDelegateCallback, where MvpDelegateCallback is the MvpActivity itself. You can also add logic to the Activity lifecycle methods that you want to implement.

Some of you might ask, why is there no Model? In fact, the code here is mainly for Presenter encapsulation, from the author’s official demo, you can see that the Model and View need to create their own.

Here is only a simple analysis, interested students can check its source code, again, MVP is more of an idea, not limited to a certain set, you can understand its ideas, write your OWN MVP.

Welcome to pay attention to wechat public number, receive first-hand technical dry goods