Android MVP經驗談

weixin_33866037發表於2017-05-06

歡迎轉載,轉載時請註明出處和作者
作者:kerwin
原文地址:
http://www.jianshu.com/p/284990eebf0c

****MVP->Model-View-Presenter****,意在解決業務邏輯與視覺層的解耦問題。關於MVC與MVP的區別相關資料很多,這裡不再累述,相關傳送門:
MVP模式(百度百科)

4290785-902281d462c5ece1.png

從上圖看出,Presenter是作為Model與View層互動的聯結器存在。將上圖轉化為程式碼實現的官方例項:Google官方architecture專案

MVP模式類圖

4290785-7fd3130c21e6fbcd.png

類圖解析
核心的3個介面:BaseUI、BasePresenter、XContract。

****BasePresenter****,業務實現層的基礎介面,所有Presenter的超父類,
方法:start(Bundle bundle),Presenter層的資料初始化,Bundle為常規頁面資料傳遞的基本容器。

****BaseUI****,檢視層的基礎介面,所有UI實現類的超父類
方法:setPresenter(T presenter),用於關聯UI與Presenter,主要用在Fragment與Presenter的繫結。

****XContract****,主要是為了將具體的UI和Presenter集合到一個檔案中,方便集中查閱管理。

實踐
需求:X頁面包含3個需求。1、計算總成績a+b,2、獲取X的姓名和性別,3、展示總成績和X的資訊。

1)首先按照需求寫出對應的Model類

public class XModle {

/**
 * @param a a課程分數
 * @param b b課程分數
 * @return  總分數
 */
public int sum(int a, int b) {
    return a + b;
}

/**
 * 假裝這裡是從伺服器獲取的資料
 * @param callback
 */
public void getX(Callback callback) {
    callback.onResult("kerwin", "boy");
}

public interface Callback {
    void onResult(String name, String gender);
}
}

2)建立XContract類

public interface XContract {

interface UI extends BaseUI<Presenter> {
  
}

interface Presenter extends BasePresenter {

}
}

3)根據業務需求新增UI和Presenter介面的方法

public interface XContract {
    /**
     * A課程得分的傳遞的引數名
     */
    String PARAM_INT_A = "a";
    /**
     * B課程得分的傳遞的引數名
     */
    String PARAM_INT_B = "b";

    interface UI extends BaseUI<Presenter> {

        /**
         * 通知UI,Sum已經計算出來,可以重新整理對應的UI了
         */
        void refreshSumResult();

        /**
         * 通知UI,X的資訊已經獲取到,可以重新整理X了。
         */
        void refreshX();

        /**
         * 顯示loading彈窗
         * @param msg
         */
        void showLoading(String msg);

        /**
         * 關閉loading彈窗
         */
        void dismissLoading();

    }

    interface Presenter extends BasePresenter {

        /**
         * 獲取X的姓名
         * @return
         */
        String getXName();

        /**
         * 獲取X的性別
         * @return
         */
        String getXGender();

        /**
         * 獲取總成績
         * @return
         */
        String getSumScore();
    }

}

4)實現XPresenter類

public class XPresenter implements XContract.Presenter {

    private XContract.UI ui;

    private XModle modle;

    private int a, b;

    private int sum = 0;

    private String xName, xGender;

    public XPresenter(XContract.UI ui) {
        this.ui = ui;
        this.modle = new XModle();
    }

    @Override
    public void start(Bundle bundle) {
        a = bundle.getInt(XContract.PARAM_INT_A, 0);
        b = bundle.getInt(XContract.PARAM_INT_B, 0);
        start();
    }

    private void start() {
        sum = modle.sum(a, b);
        ui.refreshSumResult();
        ui.showLoading("正在獲取X的資訊");
        modle.getX(new XModle.Callback() {
            @Override
            public void onResult(String name, String gender) {
                ui.dismissLoading();
                xName = name;
                xGender = gender;
                ui.refreshX();
            }
        });
    }

    @Override
    public String getXName() {
        return "姓名:"+xName;
    }

    @Override
    public String getXGender() {
        return "性別:"+xGender;
    }

    @Override
    public String getSumScore() {
        return "總成績"+sum;
    }
}

到這裡,可以看到,我們Activity還沒有開始寫,但是我們的業務流程已經完成了,這就是MVP的魅力。至於頁面長什麼樣子並不需要業務邏輯層去關注。現在我們理一下整個需求完成的程式流圖。

****程式流圖****

4290785-082d6646b337159c.png
計算總分數
4290785-3dcb6ea283fb3f96.png
獲取X的資訊

看過程式流圖後結合之前的程式碼,可能會問:為什麼UI裡面沒有setXName方法,為什麼Presenter裡面有getXName方法。為什麼Presenter從XModel裡面獲取到Sum返回值後不是直接呼叫ui.setSum(sum),而是ui.refreshSumResult()?

個人實踐中發現,UI層是隨時可能會變化的,但是Presenter基本不變,因為介面基本不變。所以,我推薦的MVP實踐方式是,主動的在UI中從Presenter獲取資料,而不是在Presenter中主動的修改UI資訊,Presenter完成從Model獲取資料的職責後僅需通知UI已經完成資料獲取,UI想如何顯示或顯示什麼自己決定。

****下面繼續完善UI部分的實現程式碼****

5)新增layout檔案activity_x.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp">

    <TextView
        android:id="@+id/tv_sum"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv_x_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp" />

    <TextView
        android:id="@+id/tv_x_gender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp" />

</LinearLayout>

6)新增XActivity類

public class XActivity extends Activity implements XContract.UI {

    private TextView tv_x_name;

    private TextView tv_x_gender;

    private TextView tv_sum;

    private XContract.Presenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_x);
        initView();
        initPresenter();
    }

    private void initView() {
        tv_sum = (TextView) findViewById(R.id.tv_sum);
        tv_x_name = (TextView) findViewById(R.id.tv_x_name);
        tv_x_gender = (TextView) findViewById(R.id.tv_x_gender);
    }

    private void initPresenter() {
        presenter = new XPresenter(this);
        presenter.start(getIntent().getExtras());
    }

    @Override
    public void setPresenter(XContract.Presenter presenter) {
        // 如果UI實現是一個Fragment,這裡的程式碼是需要的,是Activity的這裡留空即可
        this.presenter = presenter;
    }

    @Override
    public void refreshSumResult() {
        tv_sum.setText(presenter.getSumScore() + "");
    }

    @Override
    public void refreshX() {
        tv_x_name.setText(presenter.getXName());
        tv_x_gender.setText(presenter.getXGender());
    }

    @Override
    public void showLoading(String msg) {
        //顯示Dialog彈窗
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void dismissLoading() {
        //關閉Dialog彈窗
    }
}

至此,所有的需求全部完成了。

****說在最後****
MVP每一層都有自己的職責,請儘量做到各司其職。
幾點實踐建議:
1、View層不包含任何Model層的程式碼以及引用
2、Presenter作為聯結器,儘量不要直接呼叫UI層的具體UI更新的方法,僅需通知UI層自己去重新整理某一個細分模組(就像上面的refreshX)

示例原始碼傳送門