1. 程式人生 > >一個小例子徹底搞懂 MVP

一個小例子徹底搞懂 MVP

什麼是 MVP

MVP 全稱:Model-View-Presenter ;MVP 是從經典的模式 MVC 演變而來,它們的基本思想有相通的地方:Controller/Presenter 負責邏輯的處理,Model 提供資料,View 負責顯示。

為什麼要使用 MVP

在討論為什麼要使用 MVP 架構之前,我們首先要了解傳統的 MVC 的架構的特點及其缺點。

首先看一下 MVC 架構的模型圖,如下

mvc

這個圖很簡單,當 View 需要更新時,首先去找 Controller,然後 Controller 找 Model 獲取資料,Model 獲取到資料之後直接更新 View。

在 MVC 裡,View 是可以直接訪問 Model 的。從而,View 裡會包含 Model 資訊,不可避免的還要包括一些業務邏輯。 在 MVC 模型裡,更關注的 Model 的不變,而同時有多個對 Model 的不同顯示,即 View。所以,在 MVC 模型裡,Model 不依賴於 View,但是View 是依賴於 Model 的。不僅如此,因為有一些業務邏輯在 View 裡實現了,導致要更改 View 也是比較困難的,至少那些業務邏輯是無法重用的。

編者按:大多數情況下,View和Model都不會直接互動,而是通過Controller來間接互動。

這樣說可能會有點抽象,下面通過一個簡單的例子來說明。

假設現在有這樣一個需求,Activity 中有一個 Button 和一個 TextView,點選 Button 時會請求網路獲取一段字串,然後把字串顯示在 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) { new HttpModel(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() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); Message msg = handler.obtainMessage(); msg.obj = "從網路獲取到的資料"; handler.sendMessage(msg); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }

程式碼很簡單,當點選 Button 的時候,建立一個 HttpModel 物件,並把 TextView 物件作為引數傳入,然後呼叫它的 request 方法來請求資料,當請求到資料之後,切換到主執行緒中更新 TextView,流程完全符合上面的 MVC 架構圖。

但是這裡有個問題,首先很顯然,HttpModel 就是 Model 層,那麼 View 層和 Controller 層呢,我們分析一下 View 層和 Model 層分別幹了什麼事,在本例中,View 層主要做的事就是當獲取到網路資料的時候,更新 TextView,Controller 層主要做的事就是建立 HttpModel 物件並呼叫它的 request 方法,我們發現 MVCActivity 同時充當了 View 層和 Controller 層。

這樣會造成兩個問題,第一,View 層和 Controller 層沒有分離,邏輯比較混亂;第二,同樣因為 View 和 Controller 層的耦合,導致 Activity 或者 Fragment 很臃腫,程式碼量很大。由於本例比較簡單,所以這兩個問題都不是很明顯,如果 Activity 中的業務量很大,那麼問題就會體現出來,開發和維護的成本會很高。

如何使用 MVP

既然 MVC 有這些問題,那麼應該如何改進呢,答案就是使用 MVP 的架構,關於 MVP 架構的定義前面已經說了,下面看一下它的模型圖

mvp

這個圖也很簡單,當 View 需要更新資料時,首先去找 Presenter,然後 Presenter 去找 Model 請求資料,Model 獲取到資料之後通知 Presenter,Presenter 再通知 View 更新資料,這樣 Model 和 View 就不會直接互動了,所有的互動都由 Presenter 進行,Presenter 充當了橋樑的角色。很顯然,Presenter 必須同時持有 View 和 Model 的物件的引用,才能在它們之間進行通訊。

接下來用 MVP 的架構來改造上面的例子,程式碼如下

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() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    Message msg = handler.obtainMessage();
                    msg.obj = "從網路獲取到的資料";
                    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() {
        model.request();
    }
}

簡單解釋一下上面的程式碼,首先建立一個 MVPView 的介面,它即時 View 層,裡面有一個更新 TextView 的方法,然後讓 Activity 實現這個介面,並複寫更新 TextView 的方法。Model 層不再傳入 TextView 了,而是傳入一個回撥介面 Callback,因為網路請求獲取資料是非同步的,在獲取到資料之後需要通過 Callback 來通知 Presenter。Presenter 也很簡單,首先在它的構造方法中,同時持有 View 和 Model 的引用,再對外提供一個 request 方法。

分析一下上面程式碼執行的流程,當點選 Button 的時候,Presenter 呼叫 request 方法,在它的內部,通過 Model 呼叫 request 方法來請求資料,請求到資料之後,切換到主執行緒,呼叫 callback 的 onResult 方法來通知 Presenter,這時候 Presenter 就會呼叫 View 的 updateTv 方法來更新 TextView,完成了整個流程,可以發現,在整個過程從,View 和 Model 並沒有直接互動,所有的互動都是在 Presenter 中進行的。

注意事項

介面的必要性
可能有的同學會問,為什麼要寫一個 MVPView 的介面,直接把 Activity 本身傳入到 Presenter 不行嗎?這當然是可行的,這裡使用介面主要是為了程式碼的複用,試想一下,如果直接傳入 Activity,那麼這個 Presenter 就只能為這一個 Activity 服務。舉個例子,假設有個 App 已經開發完成了,可以在手機上正常使用,現在要求做平板上的適配,在平板上的介面顯示效果有所變化,TextView 並不是直接在 Activity 中的,而是在 Fragment 裡面,如果沒有使用 View 的介面的話,那就需要再寫一個針對 Fragment 的 Presenter,然後把整個過程再來一遍。但是使用 View 的介面就很簡單了,直接讓 Fragment 實現這個介面,然後複寫接口裡面的方法,Presenter 和 Model 層都不需要做任何改動。同理,Model 層也可以採用介面的方式來寫。

防止記憶體洩漏
其實上面的程式碼存在記憶體洩漏的風險。試想一下,如果在點選 Button 之後,Model 獲取到資料之前,退出了 Activity,此時由於 Activity 被 Presenter 引用,而 Presenter 正在進行耗時操作,會導致 Activity 的物件無法被回收,造成了記憶體洩漏,解決的方式很簡單,在 Activity 退出的時候,把 Presenter 對中 View 的引用置為空即可。

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

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

另外還有一個問題,雖然這裡 Activity 不會記憶體洩漏了,但是當 Activity 退出之後,Model 中請求資料就沒有意義了,所以還應該在 detachView 方法中,把 Handler 的任務取消,避免造成資源浪費,這個比較簡單,就不貼程式碼了。

MVP 的封裝

很顯然,MVP 的實現套路是大致相同的,如果在一個應用中,存在大量的 Activity 和 Fragment,並且都使用 MVP 的架構,那麼難免會有很多重複工作,所以封裝就很有必要性了。

在說 MVP 的封裝之前,需要強調一點,MVP 更多的是一種思想,而不是一種模式,每個開發者都可以按照自己的思路來實現具有個性化的 MVP,所以不同的人寫出的 MVP 可能會有一些差別,筆者在此僅提供一種實現思路,供讀者參考。

首先 Model、View 和 Presenter 都可能會有一些通用性的操作,所以可以分別定義三個對應的底層介面。

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() {
        model = createModel();
    }

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

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

    abstract M createModel();
}

這裡的 View 層添加了一個通用的方法,顯示錯誤資訊,寫在介面層,可以在實現處按照需求來顯示,比如有的地方可能會是彈出一個 Toast,或者有的地方需要將錯誤資訊顯示在 TextView 中,Model 層也可以根據需要新增通用的方法,重點來看一下 Presenter 層。

這裡的 BasePresenter 採用了泛型,為什麼要這麼做呢?主要是因為 Presenter 必須同時持有 View 和 Model 的引用,但是在底層介面中無法確定他們的型別,只能確定他們是 BaseView 和 BaseModel 的子類,所以採用泛型的方式來引用,就巧妙的解決了這個問題,在 BasePresenter 的子類中只要定義好 View 和 Model 的型別,就會自動引用他們的物件了。Presenter 中的通用的方法主要就是 attachView 和 detachView,分別用於建立 View 物件和把 View 的物件置位空,前面已經說過,置空是為了防止記憶體洩漏,Model 的物件可以在 Presenter 的構造方法中建立。另外,這裡的 Presenter 也可以寫成介面的形式,讀者可以按照自己的喜好來選擇。

然後看一下在業務程式碼中該如何使用 MVP 的封裝,程式碼如下

interface TestContract {

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

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

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

首先定義一個 Contract 契約介面,然後把 Model、View、和 Presenter 的子類分別放入 Contract 的內部,這裡的一個 Contract 就對應一個頁面(一個 Activity 或者一個 Fragment),放在 Contract 內部是為了讓同一個頁面的邏輯方法都放在一起,方便檢視和修改。Presenter 中的 request3 方法演示瞭如何通過 Presenter 來進行 View 和 Model 的互動。

接下來要做的就是實現這三個模組的邏輯方法了,在 Activity 或 Fragment 中實現 TextContract.View 的介面,再分別建立兩個類用來實現 TextContract.Model 和 TextContract.Presenter,複寫裡面的抽象方法就好了。

擴充套件:用 RxJava 簡化程式碼

上面的程式碼中,Model 層中的每個方法都傳入了一個回撥介面,這是因為獲取資料往往是非同步的,在獲取的資料時需要用回撥介面通知 Presenter 來更新 View。

如果想要避免回撥介面,可以採用 RxJava 的方式來 Model 獲取的資料直接返回一個 Observable,接下來用 RxJava 的方式來改造前面的例子

public class HttpModel {
    public Observable<String> request() {
        return Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                Thread.sleep(2000);
                emitter.onNext("從網路獲取到的資料");
                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() {
        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() {
        view = null;
        if (disposable != null && !disposable.isDisposed()) {
            disposable.dispose();
        }
    }
}

Model 的 request 方法直接返回一個 Observable,然後在 Presenter 中呼叫 subscribe 方法來通知 View 更新,這樣就避免了使用回撥介面。

開源庫推薦

最後,推薦一個 MVP 架構的開源庫,正如筆者所說,MVP 更多的是一種思想,所以 github 上關於 MVP 的開源庫並不多,大多是在完整的 APP 內部自己封裝的 MVP。如果想要比較簡單的整合 MVP 的架構,筆者推薦這個庫:
https://github.com/sockeqwe/mosby
它的使用方法比較簡單,可以直接參考官方的 demo,接下來簡單的分析一下作者的封裝思想。

首先 View 層和 Presenter 層分別有一個基礎的介面

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
   * <code>Activity.detachView()</code> or <code>Fragment.onDestroyView()</code>
   */
  @UiThread
  void detachView(boolean retainInstance);
}

這裡加 @UIThread 註解是為了確保 attachView 和 detachView 都執行在主執行緒中。
然後業務程式碼的 Activity 需要繼承 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() {
    super.onDestroy();
    getMvpDelegate().onDestroy();
  }

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

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

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

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

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

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

  @Override public void onContentChanged() {
    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();

  /**
   * 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() {
    if (mvpDelegate == null) {
      mvpDelegate = new ActivityMvpDelegateImpl(this, this, true);
    }

    return mvpDelegate;
  }

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

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

  @NonNull @Override public V getMvpView() {
    return (V) this;
  }
}

MvpActivity 中持有一個 ActivityMvpDelegate 物件,它的實現類是 ActivityMvpDelegateImpl,並且需要傳入 MvpDelegateCallback 介面,ActivityMvpDelegateImpl 的程式碼如下

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) {
    return keepPresenterInstance && (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() {

    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() {
    P presenter = delegateCallback.getPresenter();
    if (presenter == null) {
      throw new NullPointerException("Presenter returned from getPresenter() is null");
    }
    return presenter;
  }

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

  @Override public void onDestroy() {
    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() {

  }

  @Override public void onResume() {

  }

  @Override public void onStart() {

  }

  @Override public void onStop() {

  }

  @Override public void onRestart() {

  }

  @Override public void onContentChanged() {

  }

  @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) {
  }
}

程式碼有點長,但是邏輯還是比較清晰的,它其實就是在 onCreate 方法中根據不同的情況來建立 Presenter 物件,並通過 MvpDelegateCallback 的 setPresenter 方法把它儲存在 MvpDelegateCallback 中,這裡的 MvpDelegateCallback 就是 MvpActivity 本身。另外可以在 Activity 的各個生命週期方法中加入需要實現的邏輯。

可能有的同學會問,為什麼沒有 Model 呢?其實這裡的程式碼主要是對 Presenter 的封裝,從作者給出的官方 demo 中可以發現,Model 和 View 都是需要自己建立的。

這裡只做一個簡單的分析,有興趣的同學可以自己檢視它的原始碼,再強調一遍,MVP 更多的是一種思想,不用侷限於某一種套路,可以在領悟了它的思想之後,寫出自己的 MVP。

歡迎關注微信公眾號,接收第一手技術乾貨: