MVC,MVP,MVPVM(一)實踐之路

文章目錄

1.MVC,MVP,MVPVM(一)實踐之路
2.MVC,MVP,MVPVM(二)提升效率之Templates

簡介

分別使用MVC,MVP,MVP+VM,實踐具體需求,對比優劣,逐步優化。

需求

實現我的押金頁面,包含未繳納,已繳納,免押金3種狀態
1.頂部title:3種狀態展示不同文案;
2.金額:已繳納,未繳納狀態金額字號,色值不同;免押金狀態不展示;
3.底部tips:已繳納,免押金狀態展示不同文案;已繳納狀態,不展示;
4.按鈕:未繳納,已繳納狀態,文案,及點擊事件都不相同;

我的押金頁面.png
我的押金頁面.png
MVC的實現方式

activity_main.xml

<LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingLeft="30dp"
            android:paddingRight="30dp"
            tools:context="com.listen.test_mvc.MainActivity">
        
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:textColor="@android:color/black"
                android:textSize="20sp"
                tools:text="您需要繳納押金"/>
        
            <TextView
                android:id="@+id/tv_money"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="50dp"
                android:layout_marginTop="50dp"
                android:textColor="@android:color/darker_gray"
                android:textSize="40sp"
                tools:text="¥200"/>
        
            <TextView
                android:id="@+id/tv_tips"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="16sp"
                tools:text="押金隨時可退"/>
        
            <Button
                android:id="@+id/btn_pay_or_return"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="30dp"
                android:layout_marginRight="30dp"
                android:layout_marginTop="100dp"
                android:textColor="@android:color/black"
                android:textSize="16sp"
                tools:text="繳納押金"/>
        </LinearLayout>

在MainActivity中通過butterKnife框架初始化view

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_title)
    TextView mTvTitle;
    @BindView(R.id.tv_money)
    TextView mTvMoney;
    @BindView(R.id.tv_tips)
    TextView mTvTips;
    @BindView(R.id.btn_pay_or_return)
    Button mBtnPayOrReturn;

    ///////////////////////////////////////////////////////////////////////////
    // 繳納押金,退還押金的點擊事件
    ///////////////////////////////////////////////////////////////////////////
    private View.OnClickListener mDepositPayClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "繳納押金", Toast.LENGTH_SHORT).show();
        }
    };

    private View.OnClickListener mDepositReturnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "退還押金", Toast.LENGTH_SHORT).show();
        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }
}

定義IDepositRepository封裝數據獲取邏輯(server,sqlite),此處模擬網絡請求

public interface IDepositRepository {
    void getDepositInfo();
}
public interface OnDepositLoadListener {
    void onLoadDepositSuccess(MyDepositModel model);
}
public class DepositRepositoryImpl implements IDepositRepository {

    private OnDepositLoadListener mOnDepositLoadListener;

    public DepositRepositoryImpl(OnDepositLoadListener onDepositLoadListener) {
        mOnDepositLoadListener = onDepositLoadListener;
    }

    public void getDepositInfo() {
        new HttpTask() {
            @Override
            public void onRequestSuccess(MyDepositModel model) {
                mOnDepositLoadListener.onLoadDepositSuccess(model);
            }
        }.path("http://xxxx/getMydeposit").execute();
    }
}

MyDepositModel用于存儲數據

public class MyDepositModel {
    public String moneyPaied;// 已經繳納押金時,該字段表示已經繳納的金額
    public String moneyNeed; // 未繳納押金時,該字段表示需要繳納的金額
    public String isDepositPay;// 是否繳納押金,1:是,0:否
    public String isAuth; // 是否實名認證,1:是,0:否

    public static MyDepositModel mock() {
        MyDepositModel model = new MyDepositModel();
        model.moneyPaied = "200.00";
        model.moneyNeed = "300.00";
        model.isDepositPay = "0";
        model.isAuth = "0";
        return model;
    }

    public boolean isDepositPay() {
        return "1".equals(isDepositPay);
    }

    public boolean isAuth() {
        return "1".equals(isAuth);
    }
}

在MainActivity中調用IDepositRepository請求數據,通過OnDepositLoadListener獲取請求成功后的數據,根據數據展示不同的view

public class MainActivity extends AppCompatActivity implements OnDepositLoadListener {

    @BindView(R.id.tv_title)
    TextView mTvTitle;
    @BindView(R.id.tv_money)
    TextView mTvMoney;
    @BindView(R.id.tv_tips)
    TextView mTvTips;
    @BindView(R.id.btn_pay_or_return)
    Button mBtnPayOrReturn;
    
    private IDepositRepository mIDepositRepositoryImpl;

    ///////////////////////////////////////////////////////////////////////////
    // 繳納押金,退還押金的點擊事件
    ///////////////////////////////////////////////////////////////////////////
    private View.OnClickListener mDepositPayClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "繳納押金", Toast.LENGTH_SHORT).show();
        }
    };

    private View.OnClickListener mDepositReturnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "退還押金", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        mIDepositRepositoryImpl = new DepositRepositoryImpl(this);
        requestData();
    }

    ///////////////////////////////////////////////////////////////////////////
    // 模擬請求網絡數據
    ///////////////////////////////////////////////////////////////////////////
    private void requestData() {
        mIDepositRepositoryImpl.getDepositInfo();
    }

    ///////////////////////////////////////////////////////////////////////////
    // 請求數據后的回調
    ///////////////////////////////////////////////////////////////////////////
    @Override
    public void onLoadDepositSuccess(MyDepositModel model) {
        showMydepositView(model);
    }

    ///////////////////////////////////////////////////////////////////////////
    // 根據數據的不同狀態展示不同的view
    ///////////////////////////////////////////////////////////////////////////
    private void showMydepositView(MyDepositModel model) {
        if (model.isAuth()) {
            // 已經實名認證
            showAuthView();
        } else if (model.isDepositPay()) {
            // 已經繳納押金
            showDepositPaiedView(model);
        } else {
            // 未繳納押金
            showDepositNoPaiedView(model);
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // 展示未繳納押金view
    ///////////////////////////////////////////////////////////////////////////
    private void showDepositNoPaiedView(MyDepositModel model) {
        // title
        mTvTitle.setText("您需要繳納押金");

        // money
    mTvMoney.setTextColor(getResources().getColor(android.R.color.darker_gray));
        mTvMoney.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 30);
        mTvMoney.setText("¥ " +model.moneyNeed);

        // tips
        mTvTips.setText("押金隨時可退");

        //button
        mBtnPayOrReturn.setText("繳納押金");
        mBtnPayOrReturn.setOnClickListener(mDepositPayClickListener);
    }

    ///////////////////////////////////////////////////////////////////////////
    // 展示繳納押金view
    ///////////////////////////////////////////////////////////////////////////
    private void showDepositPaiedView(MyDepositModel model) {
        // title
        mTvTitle.setText("您當前押金");

        // money
mTvMoney.setTextColor(getResources().getColor(android.R.color.holo_red_light));
        mTvMoney.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 40);
        mTvMoney.setText("¥ " + model.moneyPaied);

        // tips
        mTvTips.setVisibility(View.INVISIBLE);

        //button
        mBtnPayOrReturn.setText("退還押金");
        mBtnPayOrReturn.setOnClickListener(mDepositReturnClickListener);
    }

    ///////////////////////////////////////////////////////////////////////////
    // 展示已實名認證view
    ///////////////////////////////////////////////////////////////////////////
    private void showAuthView() {
        // title
        mTvTitle.setText("您已享受免押金服務");

        // money
        mTvMoney.setVisibility(View.INVISIBLE);

        // tips
        mTvTips.setText("您已完成實名認證");

        //button
        mBtnPayOrReturn.setVisibility(View.INVISIBLE);
    }
}

效果圖


免押金.png
免押金.png

未繳納.png
未繳納.png

已繳納.png
已繳納.png

項目結構:
model:MydepositModel作為數據的載體,Repository負責從網絡獲取數據,兩者共同承擔著model的職責;
view:activity_main.xml負責view的展示形式;
control:MainActivity負責接收view的交互請求,提交給model;當model發生變化時操作view,更新展示邏輯。

mvc1.png
mvc1.png

mvc2.png
mvc2.png

Activity:view的容器,控制生命周期,頁面交互與事件處理
xml:view展示與布局
view邏輯:操作view進行更新,如setText,setVisible等
業務邏輯:model更新,根據返回數據,執行邏輯主線,如:已/未繳納/已認證
Repository:數據中心(server,sqlite)
model:存儲數據
交互邏輯:用戶操作view,產生事件與數據,反向傳遞給model進行處理,如setOnclick,或在EditText中輸入內容提交server等

總結:

xml作為view層,控制能力太弱,如果要去動態的改變一個Textview的字號,色值,或者隱藏/顯示一個按鈕,這些都沒辦法在xml中做,只能把代碼寫在Activity中。MyDepositModel以后,需要根據isAuth,isDepositPaied等業務邏輯,控制view的展示。造成了Activity既是view層,又是controller層,導致代碼膨脹,當業務復雜度繼續增加時,一個Activity上千行代碼是很常見的,大量邏輯參與其中,維護及代碼閱讀難度將不斷提升。
view和model直接交互,如:mTvMoney.setText(model.moneyNeed),耦合較重,無法獨立變化。mTvMoney作為一個Textview,只需要提供通過setText方法將String設置到TextView上進行展示的一種能力,至于這個String是從model1,還是model2中獲取的,mTvMoney并不關心,而model作為數據源,也同樣不需要關心當前是展示在mTvMoney上,還是mTvTips上。mBtnPayOrReturn按鈕也是一樣,只需提供一種點擊響應的能力,至于點擊后是操作繳納押金,還是退還押金,mBtnPayOrReturn并不關心。

MVP的實現方式1

在view和model之間新增presenter作為溝通的橋梁,presenter從model獲取數據后,更新view的展示,使得view和model之間沒有耦合,也將業務邏輯從view上抽離出來。

實現MainPresenter,持有IMainView,IDepositRepository成員變量,獲取數據,根據業務邏輯更新view的展示。

public interface IMainPresenter {
    /**
     * @desc 進入頁面后刷新數據
     */
    void requestData();

    /**
     * @desc 點擊按鈕
     */
    void onButtonClickAction();
}

/**
 * @author listen
 * @desc 主頁面的presenter
 */
public class MainPresenter implements IMainPresenter, OnDepositLoadListener {

    private IMainView mIMainView;
    private IDepositRepository mIDepositRepositoryImpl;
    private MyDepositModel mModel;

    public MainPresenter(IMainView iMainView) {
        mIMainView = iMainView;
        mIDepositRepositoryImpl = new DepositRepositoryImpl(this);
    }

    @Override
    public void requestData() {
        mIDepositRepositoryImpl.getDepositInfo();
    }

    /**
     * @desc 實現OnDepositLoadListener回調,獲取MyDepositModel,并根據業務邏輯更新view的展示
     */
    @Override
    public void onLoadDepositSuccess(MyDepositModel model) {
        if (model.isAuth()) {
            // 已經實名認證
            showAuthView();
        } else if (model.isDepositPay()) {
            // 已經繳納押金
            showDepositPaiedView(model);
        } else {
            // 未繳納押金
            showDepositNoPaiedView(model);
        }
    }

    /**
     * @desc 未支付狀態view
     */
    private void showDepositNoPaiedView(MyDepositModel model) {
        // title
        mIMainView.setTitleText("您需要繳納押金"); // 不暴露mTvTitle,只提供設置title文案的能力

        // money
        mIMainView.setMoneyTextVisible();// 提供操作MoneyText顯示/隱藏的能力
        mIMainView.setMoneyTextColorGray();// 提供MoneyText字體設置為灰色的能力
        mIMainView.setMoneyTextSizeSmall();// 提供MoneyText字號設置小的能力
        mIMainView.setMoneyText("¥ " + model.moneyNeed);// 提供設置MoneyText文案的能力

        // tips
        mIMainView.setTipsTextVisible();// 提供操作TipsText顯示/隱藏的能力
        mIMainView.setTipsText("押金隨時可退");// 提供設置TipsText文案的能力

        //button
        mIMainView.setButtonVisible();// 提供操作Button顯示/隱藏的能力
        mIMainView.setButtonText("繳納押金");// 提供設置Button文案的能力
    }

    /**
     * @desc 支付狀態view
     */
    private void showDepositPaiedView(MyDepositModel model) {
        // title
        mIMainView.setTitleText("您當前押金");

        // money
        mIMainView.setMoneyTextVisible();
        mIMainView.setMoneyTextColorRed();
        mIMainView.setMoneyTextSizeBig();
        mIMainView.setMoneyText("¥ " + model.moneyNeed);

        // tips
        mIMainView.setTipsTextInvisible();

        //button
        mIMainView.setButtonVisible();
        mIMainView.setButtonText("退還押金");
    }

    /**
     * @desc 實名認證狀態view
     */
    private void showAuthView() {
        // title
        mIMainView.setTitleText("您已享受免押金服務");

        // money
        mIMainView.setMoneyTextInvisible();

        // tips
        mIMainView.setTipsTextVisible();
        mIMainView.setTipsText("您已完成實名認證");

        //button
        mIMainView.setButtonInvisible();
    }

    /**
     * @desc 當點擊Button時觸發的操作
     */
    @Override
    public void onButtonClickAction() {
        if (mModel.isDepositPay()) {
            mIMainView.showToast("退還押金");
        } else {
            mIMainView.showToast("繳納押金");
        }
    }
}

實現view層

/**
 * @author listen
 * @desc 主頁面view層接口
 */
public interface IMainView {
    void setTitleText(String text);
    
    void setMoneyTextColorGray();
    void setMoneyTextSizeSmall();
    void setMoneyText(String text);
    void setMoneyTextInvisible();
    void setMoneyTextVisible();
    void setMoneyTextColorRed();
    void setMoneyTextSizeBig();
    
    void setTipsText(String text);
    void setTipsTextInvisible();
    void setTipsTextVisible();
    
    void setButtonText(String text);
    void setButtonInvisible();
    void setButtonVisible();
    
    void showToast(String text);
}
public class MainActivity extends AppCompatActivity implements IMainView {
    @BindView(R.id.tv_title)
    TextView mTvTitle;
    @BindView(R.id.tv_money)
    TextView mTvMoney;
    @BindView(R.id.tv_tips)
    TextView mTvTips;
    @BindView(R.id.btn_pay_or_return)
    Button mBtnPayOrReturn;

    private IMainPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        mPresenter = new MainPresenter(this);
        mPresenter.requestData();

        mBtnPayOrReturn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.onButtonClickAction();
            }
        });
    }

    @Override
    public void setTitleText(String text) {
        mTvTitle.setText(text);
    }

    @Override
    public void setMoneyTextColorGray() {
        mTvMoney.setTextColor(getResources().getColor(android.R.color.darker_gray));
    }

    @Override
    public void setMoneyTextColorRed() {
        mTvMoney.setTextColor(getResources().getColor(android.R.color.holo_red_light));
    }

    @Override
    public void setMoneyTextSizeSmall() {
        mTvMoney.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 30);
    }

    @Override
    public void setMoneyTextSizeBig() {
        mTvMoney.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 40);
    }

    @Override
    public void setMoneyText(String text) {
        mTvMoney.setText(text);
    }

    @Override
    public void setMoneyTextInvisible() {
        mTvMoney.setVisibility(View.VISIBLE);
    }

    @Override
    public void setMoneyTextVisible() {
        mTvMoney.setVisibility(View.VISIBLE);
    }

    @Override
    public void setTipsText(String text) {
        mTvTips.setText(text);
    }

    @Override
    public void setTipsTextInvisible() {
        mTvTips.setVisibility(View.INVISIBLE);
    }

    @Override
    public void setTipsTextVisible() {
        mTvTips.setVisibility(View.VISIBLE);
    }

    @Override
    public void setButtonText(String text) {
        mBtnPayOrReturn.setText(text);
    }

    @Override
    public void setButtonInvisible() {
        mBtnPayOrReturn.setVisibility(View.INVISIBLE);
    }

    @Override
    public void setButtonVisible() {
        mBtnPayOrReturn.setVisibility(View.VISIBLE);
    }

    @Override
    public void showToast(String text) {
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
    }
}
mvp項目結構.png
mvp項目結構.png
mvp2.png
mvp2.png
總結

presenter處理業務邏輯并更新view,Activity只提供基礎的操作view的能力,2者互相獨立,view與業務分離。
業務變化1:不管已繳納,未繳納,免押金任何狀態tipsText都不顯示,此時就不需要去修改Activity,直接在presenter設置mIMainView.setTipsTextVisible()即可;
業務變化2:新增一種狀態,已實名認證,不過某些條件不滿足,押金不能全免,只能減半,titleText顯示"已認證,還需繳納押金",moneyText大號字體,紅色,tipsText顯示"押金已減半",Button顯示文案"補足押金",這種場景下,就不用去修改Activity的任何代碼,只要在presenter新增邏輯分支,根據view提供的能力進行更新即可。

/**
 * @desc 押金減半狀態view
 */
private void showDepositHalfPayView(MyDepositModel model) {
   // title
   mIMainView.setTitleText("已認證,還需繳納押金");

   // money
   mIMainView.setMoneyTextVisible();
   mIMainView.setMoneyTextColorRed();
   mIMainView.setMoneyTextSizeBig();
   mIMainView.setMoneyText("¥ " + model.moneyNeed);

   // tips
   mIMainView.setTipsTextVisible();
   mIMainView.setTipsText("押金已減半");

   //button
   mIMainView.setButtonVisible();
   mIMainView.setButtonText("補足押金");
}
/**
 * @desc 押金減半時,button的點擊響應
 */
public void onButtonClickAction() {
        if ("押金減半") {
            mIMainView.showToast("補足押金");
        } 
    }

業務變化3:presenter依賴的是IMainView,不管是MainActivity,還是Main1Activity,只要是實現了IMainView即可復用當前presenter。

當我們把業務邏輯抽取到presenter后,Activity基本上只剩下一些view的邏輯,真正實現了減負,變成了一個相對純凈的view。當我們需要修改view的邏輯時,就去找Activity,需要修改數據邏輯時,就去找Repository,修改業務邏輯時就去找presenter,每個模塊職責分明。
缺點:
1.view與presenter之間交互過于頻繁,Activity中都是一些setText,setVisibility等方法。這時很容易讓人想到使用Databinding可以很好的簡化這部分代碼。

MVP的實現方式2

通過DataBinding實現model到view的單向綁定,減少view與model之間因頻繁交互而產生的冗余代碼。

在<data>標簽中引入data=MyDepositModel,presenter=IMainPresenter。當model變化時,通過data將數據映射到view上。當Button產生點擊事件時交由presenter響應并處理。使用Databinding以后,開發流程上省略了findView,setView的過程,在寫xml的時候就可以直接將model進行關聯及映射。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="data" type="com.listen.test_mvvm.model.data.MyDepositModel"/>
        <variable name="presenter" type="com.listen.test_mvvm.presenter.IMainPresenter"/>
        <import type="android.view.View"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="30dp"
        android:paddingRight="30dp"
        tools:context="com.listen.test_mvvm.view.MainActivity">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:textColor="@android:color/black"
            android:textSize="20sp"
            android:text="@{data.title}"/>

        <TextView
            android:id="@+id/tv_money"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="50dp"
            android:layout_marginTop="50dp"
            android:textColor="@{data.isDepositPay ? @android:color/holo_red_light : @android:color/darker_gray}"
            android:textSize="@{data.isDepositPay ? @dimen/sp_40 : @dimen/sp_30}"
            android:visibility="@{data.isAuth ? View.INVISIBLE : View.VISIBLE}"
            android:text="@{data.money}"/>

        <TextView
            android:id="@+id/tv_tips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:visibility="@{data.showTips ? View.VISIBLE : View.INVISIBLE}"
            android:text="@{data.tips}"/>

        <Button
            android:id="@+id/btn_pay_or_return"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dp"
            android:layout_marginRight="30dp"
            android:layout_marginTop="100dp"
            android:onClick="@{presenter.onButtonClickAction}"
            android:visibility="@{data.isAuth ? View.INVISIBLE : View.VISIBLE}"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:text='@{data.isDepositPay ? "退還押金" : "繳納押金"}'/>
    </LinearLayout>
</layout>

將業務邏輯轉移到MyDepositModel

public class MyDepositModel {
    public String moneyPaied;// 已經繳納押金時,該字段表示已經繳納的金額
    public String moneyNeed; // 未繳納押金時,該字段表示需要繳納的金額
    public String isDepositPay;// 是否繳納押金,1:是,0:否
    public String isAuth; // 是否實名認證,1:是,0:否

    public boolean isDepositPay() {
        return "1".equals(isDepositPay);
    }

    public boolean isAuth() {
        return "1".equals(isAuth);
    }

    public String getTitle() {
        if (isAuth()) {
            return "您已享受免押金服務";
        }

        if (isDepositPay()) {
            return "您當前押金";
        } else {
            return "您需要繳納押金";
        }
    }

    public String getMoney() {
        if (isDepositPay()) {
            return "¥ " + moneyPaied;
        } else {
            return "¥ " + moneyNeed;
        }
    }

    public String getTips() {
        if (isAuth()) {
            return "您已完成實名認證";
        }

        if (!isDepositPay()) {
            return "押金隨時可退";
        }

        return "";
    }

    public boolean isShowTips() {
        if (isAuth() || !isDepositPay()) {
            return true;
        }
        return false;
    }
}

MainPresenter不再與view頻繁的交互,僅僅是作為view和model的連接器,主干邏輯更為清晰

public class MainPresenter implements IMainPresenter, OnDepositLoadListener {
    private IMainView mIMainView;
    private IDepositRepository mIDepositRepository;
    private MyDepositModel mModel;

    public MainPresenter(IMainView iMainView) {
        mIMainView = iMainView;
        mIDepositRepository = new DepositRepository(this);
    }

    // 請求數據
    @Override
    public void requestData() {
        mIDepositRepository.getDepositInfo();
    }

    // 獲取數據,通知view更新
    @Override
    public void onLoadDepositSuccess(MyDepositModel model) {
        mModel = model;
        mIMainView.updateData(model);
    }

    // 接收并處理view的點擊事件
    @Override
    public void onButtonClickAction(View v) {
        if (mModel.isDepositPay()) {
            mIMainView.showToast("退還押金");
        } else {
            mIMainView.showToast("繳納押金");
        }
    }
}

MainActivity中不再需要fingViewById,也不用定義Textview,Button的成員變量,全部交由DataBinding進行處理,相較MVP的實現,MainActivity進一步簡化

public class MainActivity extends AppCompatActivity implements IMainView {
    private IMainPresenter mPresenter;
    private ActivityMainBinding mBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mPresenter = new MainPresenter(this);
        mBinding.setPresenter(mPresenter);
        
        // 初始化頁面數據
        mPresenter.requestData();
    }

    // 更新數據綁定
    @Override
    public void updateData(MyDepositModel model) {
        mBinding.setData(model);
    }

    // 提供Toast提示的能力
    @Override
    public void showToast(String text) {
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
    }
}

IMainView接口也不再需要提供那么多操作view的方法

public interface IMainView {
    void updateData(MyDepositModel model);
    void showToast(String text);
}

問題:
1.xml中參雜了一些業務邏輯,如:data.isDepositPay,data.isAuth,xml中應該盡量只是簡單的view邏輯,與業務邏輯隔離。
2.由于使用databinding是model->view的單向綁定,不得不將大部分邏輯搬移到model中,例如:MyDepositModel中即有數據處理邏輯,isDepositPay,isAuth(如果model中存在list<Item>等,經常會對外提供getItemById(int id)等方法,做遍歷查詢)。同時還存在view的展示邏輯,例:isShowTips,getTitle,getMoney,這些方法都是根據數據變化控制view的展示,兩者之間其實還是有比較明確的分界線,可以進一步分離,解耦,避免model過重。

MVPVM的實現方式

通過viewModel作為model和view的適配層,model只負責數據存儲

activity_main.xml中將原先的model.isDepositPay(),model.isAuth()改成viewModel.moneyTextVisible(),viewModel.moneyTextSizeLarge()等。在xml中依賴viewModel,只關心view顯示/隱藏,字號變大/小,色值高亮/正常,至于什么情況下展示高亮,是否顯示由viewModel中適配的model邏輯決定。

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="data" type="com.listen.test_mvvm.model.viewmodel.IMyDepositViewModel"/>
        <variable name="presenter" type="com.listen.test_mvvm.presenter.IMainPresenter"/>
        <import type="android.view.View"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="30dp"
        android:paddingRight="30dp"
        tools:context="com.listen.test_mvvm.view.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@{data.title}"
            android:textColor="@android:color/black"
            android:textSize="20sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="50dp"
            android:layout_marginTop="50dp"
            android:text="@{data.money}"
            android:textColor="@{data.moneyTextColorHightLight ? @android:color/holo_red_light : @android:color/darker_gray}"
            android:textSize="@{data.moneyTextSizeLarge ? @dimen/sp_40 : @dimen/sp_30}"
            android:visibility="@{data.moneyTextVisible ? View.VISIBLE : View.INVISIBLE}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{data.tips}"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:visibility="@{data.tipsVisible ? View.VISIBLE : View.INVISIBLE}"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dp"
            android:layout_marginRight="30dp"
            android:layout_marginTop="100dp"
            android:onClick="@{presenter.onButtonClickAction}"
            android:text="@{data.buttonText}"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:visibility="@{data.buttonVisible ? View.VISIBLE : View.INVISIBLE}"/>
    </LinearLayout>
</layout>

IMyDepositViewModel接口,定義view提供的能力

public interface IMyDepositViewModel {
    String getTitle();

    boolean isMoneyTextColorHightLight();
    boolean isMoneyTextSizeLarge();
    boolean isMoneyTextVisible();
    String getMoney();

    boolean isTipsVisible();
    String getTips();

    boolean isButtonVisible();
    String getButtonText();
}

MyDepositBaseViewModel實現IMyDepositViewModel的默認展示邏輯

public abstract class MyDepositBaseViewModel implements IMyDepositViewModel {

    private MyDepositModel mModel;

    public MyDepositBaseViewModel(MyDepositModel model) {
        mModel = model;
    }

    public MyDepositModel getModel() {
        return mModel;
    }

    @Override
    public String getTitle() {
        return "";
    }

    @Override
    public boolean isMoneyTextColorHightLight() {
        return false;
    }

    @Override
    public boolean isMoneyTextSizeLarge() {
        return false;
    }

    @Override
    public boolean isMoneyTextVisible() {
        return false;
    }

    @Override
    public String getMoney() {
        return "";
    }

    @Override
    public boolean isTipsVisible() {
        return false;
    }

    @Override
    public String getTips() {
        return "";
    }

    @Override
    public boolean isButtonVisible() {
        return false;
    }

    @Override
    public String getButtonText() {
        return "";
    }
}

已繳納押金時viewModel的展示邏輯

public class MyDepositPayViewModel extends MyDepositBaseViewModel {
    public MyDepositPayViewModel(MyDepositModel model) {
        super(model);
    }

    @Override
    public String getTitle() {
        return "您當前押金";
    }

    @Override
    public String getMoney() {
        return "¥ " + getModel().moneyPaied;
    }

    @Override
    public boolean isMoneyTextVisible() {
        return true;
    }

    @Override
    public boolean isMoneyTextColorHightLight() {
        return true;
    }

    @Override
    public boolean isMoneyTextSizeLarge() {
        return true;
    }

    @Override
    public boolean isButtonVisible() {
        return true;
    }

    @Override
    public String getButtonText() {
        return "退還押金";
    }
}

未繳納押金時viewModel的展示邏輯

public class MyDepositNoPayViewModel extends MyDepositBaseViewModel {

    public MyDepositNoPayViewModel(MyDepositModel model) {
        super(model);
    }

    @Override
    public String getTitle() {
        return "您需要繳納押金";
    }

    @Override
    public String getMoney() {
        return "¥ " + getModel().moneyNeed;
    }

    @Override
    public boolean isMoneyTextVisible() {
        return true;
    }

    @Override
    public boolean isMoneyTextColorHightLight() {
        return false;
    }

    @Override
    public boolean isMoneyTextSizeLarge() {
        return false;
    }

    @Override
    public boolean isButtonVisible() {
        return true;
    }

    @Override
    public boolean isTipsVisible() {
        return true;
    }

    @Override
    public String getTips() {
        return "押金隨時可退";
    }

    @Override
    public String getButtonText() {
        return "繳納押金";
    }
}

已認證時viewModel的展示邏輯

public class MyDepositAuthViewModel extends MyDepositBaseViewModel {

    public MyDepositAuthViewModel(MyDepositModel model) {
        super(model);
    }

    @Override
    public String getTitle() {
        return "您已享受免押金服務";
    }

    @Override
    public boolean isTipsVisible() {
        return true;
    }

    @Override
    public String getTips() {
        return "您已完成實名認證";
    }
}

MainPresenter獲取數據后,根據不同業務邏輯展示
MyDepositAuthViewModel,MyDepositPayViewModel,MyDepositNoPayViewModel。此處有點像設計模式中的策略模式,這3個viewModel就是view的不同展示策略的封裝。

public class MainPresenter implements IMainPresenter, OnDepositLoadListener {

    private IMainView mIMainView;
    private IDepositRepository mIDepositRepositoryImpl;
    private MyDepositModel mModel;

    public MainPresenter(IMainView iMainView) {
        mIMainView = iMainView;
        mIDepositRepositoryImpl = new DepositRepositoryImpl(this);
    }

    @Override
    public void requestData() {
        mIDepositRepositoryImpl.getDepositInfo();
    }

    @Override
    public void onLoadDepositSuccess(MyDepositModel model) {
        mModel = model;
        if (mModel.isAuth()) {
            mIMainView.updateData(new MyDepositAuthViewModel(model));
        } else if (mModel.isDepositPay()) {
            mIMainView.updateData(new MyDepositPayViewModel(model));
        } else {
            mIMainView.updateData(new MyDepositNoPayViewModel(model));
        }
    }

    @Override
    public void onButtonClickAction(View v) {
        if (mModel.isDepositPay()) {
            mIMainView.showToast("退還押金");
        } else {
            mIMainView.showToast("繳納押金");
        }
    }
}

MainActivity.java,只做基本的數據請求,DataBinding初始化,toast提示等操縱。

public class MainActivity extends AppCompatActivity implements IMainView {
    private IMainPresenter mPresenter;
    private ActivityMainBinding mBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mPresenter = new MainPresenter(this);
        mBinding.setPresenter(mPresenter);
        mPresenter.requestData();
    }

    @Override
    public void updateData(IMyDepositViewModel viewModel) {
        mBinding.setData(viewModel);
    }

    @Override
    public void showToast(String text) {
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
    }
}
mvpvm.png
mvpvm.png

mvpvm2.png
mvpvm2.png

如圖:用戶操作view,觸發事件響應,通過presenter中轉,傳遞給model進行數據處理,獲取新數據后處理業務邏輯,并適配成不同狀態的viewModel展示策略,view根據不同的viewModel進行更新。

總結:
從mvc到mvpvm,項目中類雖然變多了,不過模塊之間職責更加明確清晰。大部分情況,使用mvp結合databinding就可以較好的對view和model進行解耦,且代碼冗余較少,當然在頁面邏輯簡單的情況下,可能連Presenter都沒有用上的必要。不過如果是類似本文中的需求,view狀態相對復雜的情況下,最好還是經過一層viewModel適配,也可以釋放model的壓力,xml布局中只依賴抽象的IMyDepositViewModel(model->view的數據輸入)和IMainPresenter(view->model的事件輸出),不依賴具體。
本文并非按照傳統的MVC,MVP,MVVM的路線實現架構,而是采用循序漸進的方式,在MVC中發現Activity過重,所以引入MVP,Presenter作為View和Model的中轉,達到解耦的目的。后來發現Activity提供view能力時冗余代碼過多,所以引入DataBinding,雖然代碼簡化了,不過xml中引入了部分業務邏輯,model中同時參雜數據處理邏輯和view展示邏輯,故而引入viewModel,將xml與model進一步解耦,同時減輕model負擔,不過此時并不算是mvvm,本質上在mvp的基礎上,引入vm,因此presenter的中轉作用還在,所以才演變成了現在的mvpvm。同時強調下,架構無絕對的好壞與絕對的標準,大家應該在項目中根據實際場景選擇最合適的架構方式。本文中如有說明,解釋不到位的地方,還請指出,互相學習共勉。

最終版本項目地址:https://github.com/listen2code/Test_MVPVM

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容