文章目錄
1.MVC,MVP,MVPVM(一)實踐之路
2.MVC,MVP,MVPVM(二)提升效率之Templates
簡介
分別使用MVC,MVP,MVP+VM,實踐具體需求,對比優劣,逐步優化。
需求
實現我的押金頁面,包含未繳納,已繳納,免押金3種狀態
1.頂部title:3種狀態展示不同文案;
2.金額:已繳納,未繳納狀態金額字號,色值不同;免押金狀態不展示;
3.底部tips:已繳納,免押金狀態展示不同文案;已繳納狀態,不展示;
4.按鈕:未繳納,已繳納狀態,文案,及點擊事件都不相同;

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);
}
}
效果圖



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


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();
}
}


總結
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();
}
}


如圖:用戶操作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。同時強調下,架構無絕對的好壞與絕對的標準,大家應該在項目中根據實際場景選擇最合適的架構方式。本文中如有說明,解釋不到位的地方,還請指出,互相學習共勉。