Android常用的開發(fā)模式包括MVC宝踪,MVP以及MVVM。標(biāo)準(zhǔn)MVC模式不適用于Android的開發(fā)联四,在標(biāo)準(zhǔn)的MVC開發(fā)模式中(如網(wǎng)絡(luò)請(qǐng)求的服務(wù)器開發(fā))境蜕,action(一個(gè)URL請(qǐng)求)首先被Controller接收,Controller讀取Model的數(shù)據(jù)汽馋,生成View并返回茫陆。但是在Android中蕾总,Activity/Fragment作為交互的起點(diǎn)收苏,代表的是View而不是Controller台诗,單純的套用MVC模式會(huì)使得Activity/Fragment中混雜Controller層的代碼,不利于維護(hù)和測(cè)試铁蹈。相比之下宽闲,MVP和MVVM更易于實(shí)現(xiàn)View層和邏輯代碼的分離,本文將通過樣例代碼對(duì)MVP和MVVM兩種模式進(jìn)行講解。
MVP
MVP包括Model容诬,View和Presenter三部分娩梨,通過Presenter層將View和Model隔離開。View和Presenter互相持有對(duì)方的引用览徒,可以互相調(diào)用狈定。Presenter持有Model的引用,可以調(diào)用Model的方法吱殉,Model可以通過Presenter的回調(diào)函數(shù)提醒某個(gè)事件的結(jié)束掸冤,如數(shù)據(jù)加載成功或失敗厘托,交互圖如下圖所示:
代碼示例:
代碼結(jié)構(gòu)如下:
在MVP開發(fā)模式中友雳,對(duì)View的操作都是通過接口(Interface)實(shí)現(xiàn)的,對(duì)應(yīng)于Demo中的MvpDemoViewBase:
public interface MvpDemoViewBase {
void updateFirstNameView(String firstName);
void updateLastNameView(String lastName);
void showToastInfo(String toast);
}
該接口定義了三個(gè)操作View的函數(shù)铅匹,updateFirstNameView押赊,updateLastNameView和showToastInfo。
作為View的MvpDemoActivity類實(shí)現(xiàn)該接口包斑,提供三個(gè)函數(shù)的具體實(shí)現(xiàn):
public class MvpDemoActivity extends AppCompatActivity implements MvpDemoViewBase {
...
@Override
public void updateFirstNameView(String firstName) {
mFirstNameTV.setText("First name: " + firstName);
}
@Override
public void updateLastNameView(String lastName) {
mLastNameTV.setText("Last name: " + lastName);
}
@Override
public void showToastInfo(String toast) {
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();
}
...
}
在onCreate函數(shù)中初始化Presenter:
public class MvpDemoActivity extends AppCompatActivity implements MvpDemoViewBase {
private MvpDemoActivityPresenter mPresenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
mPresenter = new MvpDemoActivityPresenter(this);
}
通過Presenter的引用發(fā)起數(shù)據(jù)請(qǐng)求操作:
@OnClick(R.id.load_button)
protected void onClickLoad(View v) {
mPresenter.loadUserData();
}
Presenter持有View和Model的引用流礁,從Model加載數(shù)據(jù),并根據(jù)返回?cái)?shù)據(jù)更新View:
public class MvpDemoActivityPresenter implements MvpLoadDataCallBack {
private MvpDemoViewBase view;
private MvpUserModel userModel;
public MvpDemoActivityPresenter(MvpDemoViewBase view) {
this.view = view;
userModel = new MvpUserModel();
}
// 通過Model加載數(shù)據(jù)
public void loadUserData() {
userModel.loadUserDataFromNet(this);
}
// 加載數(shù)據(jù)完成后的回調(diào)函數(shù)
@Override
public void onLoadSuccess() {
// 通過View更新界面
view.updateFirstNameView(userModel.firstName);
view.updateLastNameView(userModel.lastName);
view.showToastInfo("加載成功");
}
@Override
public void onLoadFail() {}
}
Model層實(shí)現(xiàn)對(duì)數(shù)據(jù)的定義和加載罗丰,并在加載完成后調(diào)用Presenter層的回調(diào)函數(shù):
public class MvpUserModel {
public String firstName;
public String lastName;
public MvpUserModel() {
this.firstName = "";
this.lastName = "";
}
public void loadUserDataFromNet(MvpLoadDataCallBack callBack) {
// todo: 這里省略了網(wǎng)絡(luò)請(qǐng)求的過程
this.firstName = "Jack";
this.lastName = "Wang";
// 請(qǐng)求完成調(diào)用Presenter層回調(diào)函數(shù)神帅,通過Presenter層實(shí)現(xiàn)對(duì)View的更新
callBack.onLoadSuccess();
}
}
優(yōu)點(diǎn):
1. 三層結(jié)構(gòu)比較清晰
2. 可以在沒有View的時(shí)候測(cè)試Model是否能正常加載數(shù)據(jù),只需要寫一個(gè)實(shí)現(xiàn)了View接口的測(cè)試類萌抵;同理找御,可以在沒有Model的時(shí)候通過Presenter層fake數(shù)據(jù)測(cè)試View層是否正常;
缺點(diǎn):
1. 復(fù)雜的頁(yè)面View層接口可能很多绍填,增加了代碼的數(shù)量和維護(hù)成本
MVVM
MVVM通過Data Binding庫(kù)將View的元素和Model的屬性綁定起來(lái)霎桅,使得Model數(shù)據(jù)發(fā)生變化時(shí)對(duì)應(yīng)的View元素自動(dòng)更新,底層實(shí)現(xiàn)是觀察者模式讨永。Data Binding庫(kù)是一個(gè)Support庫(kù)滔驶,支持Android 2.1及以上,Gradle版本1.5.0及以上卿闹。
學(xué)會(huì)了Data Binding庫(kù)的使用揭糕,基本就了解了MVVM的使用。下面通過Demo進(jìn)行簡(jiǎn)單介紹锻霎。
代碼結(jié)構(gòu):
首先在gradle文件中添加如下行啟用Data Binding:
android {
....
dataBinding {
enabled = true
}
}
在布局文件mvvm_demo_layout.xml中添加<data>...</data>段定義數(shù)據(jù)變量:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable name="userViewModel" type="com.magic.wangdongliang.designpatterndemo.mvvm.viewmodel.MvvmUserViewModel" />
<variable name="handlers" type="com.magic.wangdongliang.designpatterndemo.mvvm.view.MvvmDemoActivity" />
</data>
...
</layout>
利用import引入Class著角,利用variable定義變量,type為變量類型量窘,name為變量名,userViewModel和handlers分布代表Model和View雇寇,這樣就可以在該xml布局文件中使用定義的變量:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
...
<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<TextView android:id="@+id/first_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp"
android:text="@{userViewModel.firstName}"
tools:text="First name: "/>
<TextView android:id="@+id/last_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp" android:layout_marginTop="30dp"
android:text="@{userViewModel.lastName}"
tools:text="Last name: "/>
<TextView android:id="@+id/is_adult_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp" android:layout_marginTop="30dp"
android:text="Is adult: Yes"
android:visibility="@{userViewModel.isAdult ? View.VISIBLE : View.GONE}" />
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加載數(shù)據(jù)"
android:layout_marginTop="30dp"
android:layout_gravity="center_horizontal"
android:onClick="@{handlers.onClickLoadData}"/>
</LinearLayout>
</layout>
完成布局文件后,Data Binding庫(kù)會(huì)自動(dòng)生成一個(gè)輔助類MvvmDemoLayoutBind,在MVVMDemoActivity中利用這個(gè)輔助類給布局中的變量賦值锨侯,并對(duì)布局中的元素進(jìn)行綁定嫩海。
public class MvvmDemoActivity extends AppCompatActivity {
private TextView mFirstNameTV;
private TextView mLastNameTV;
private TextView mIsAdultTV;
private MvvmUserViewModel userViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 給布局變量賦值
MvvmDemoLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.mvvm_demo_layout);
userViewModel = new MvvmUserViewModel();
binding.setUserViewModel(userViewModel);
binding.setHandlers(this);
// 綁定布局元素
mFirstNameTV = binding.firstNameTv;
mLastNameTV = binding.lastNameTv;
mIsAdultTV = binding.isAdultTv;
}
// 定義View響應(yīng)事件
public void onClickFirstName(View view) { Toast.makeText(this, "First name is" + mFirstNameTV.getText(), Toast.LENGTH_SHORT).show();}
public void onClickLastName(View v) { Toast.makeText(this, "Last name is" + mLastNameTV.getText(), Toast.LENGTH_SHORT).show();}
public void onClickLoadData(View v) { userViewModel.loadUserData();}
}
在MvvmUserModel中添加數(shù)據(jù)的定義和網(wǎng)絡(luò)加載過程:
public class MvvmUserModel {
public String firstName;
public String lastName;
public boolean isAdult;
public MvvmUserModel() {
firstName = "";
lastName = "";
isAdult = false;
}
public void loadUserDataFromNet(MvvmLoadDataCallBack callBack) {
// todo: 這里省略了網(wǎng)絡(luò)請(qǐng)求的過程
this.firstName = "Jack";
this.lastName = "Wang";
this.isAdult = true;
callBack.onLoadSuccess();
}
}
最后是作為ViewModel層的MvvmUserViewModel類,負(fù)責(zé)通過Model層的引用調(diào)用數(shù)據(jù)加載過程囚痴,并在回調(diào)函數(shù)中發(fā)起更新界面的消息叁怪,Data Binding框架會(huì)更新跟數(shù)據(jù)源綁定的View元素,從而實(shí)現(xiàn)界面的自動(dòng)更新深滚。
public class MvvmUserViewModel extends BaseObservable implements MvvmLoadDataCallBack {
private MvvmUserModel user;
public MvvmUserViewModel() {
user = new MvvmUserModel();
}
@Bindable
public String getFirstName() {
return "First name: " + user.firstName;
}
@Bindable
public String getLastName() {
return "Last name: " + user.lastName;
}
@Bindable
public boolean isAdult() {
return user.isAdult;
}
public void loadUserData() {
user.loadUserDataFromNet(this);
}
@Override
public void onLoadSuccess() {
notifyPropertyChanged(BR.firstName);
notifyPropertyChanged(BR.lastName);
notifyPropertyChanged(BR.adult);
// todo: 這里單純的MVVM模式如何展示一條toast變得困難, 必須配合MVP模式添加一個(gè)Presenter層才能實(shí)現(xiàn)
}
@Override
public void onLoadFail() {
}}
這里使用了Bindable注解奕谭,通過給指定的函數(shù)添加Bindable注解,Data Binding框架會(huì)根據(jù)函數(shù)名自動(dòng)生成一個(gè)BR的屬性痴荐,如BR.firstName血柳,在數(shù)據(jù)源發(fā)生變化后,可以調(diào)用notifyPropertyChanged(BR.firstName)通知fitstName的變化生兆,getFirstName()返回最新值难捌,更新所有跟firstName數(shù)據(jù)源綁定的View元素。由于我們需要通過notifyPropertyChanged通知某個(gè)或某些數(shù)據(jù)源的更新鸦难,所以MVVM模式中View隨Model的更新而更新并不是完全“自動(dòng)”完成的根吁,而是需要我們“手動(dòng)”通知的。
同時(shí)合蔽,并不是所有的數(shù)據(jù)展示都能通過Data Binding的方式完成击敌,比如最簡(jiǎn)單的展示一個(gè)Toast,或者展示一個(gè)數(shù)據(jù)列表拴事。由于ViewModel層并不持有View層的引用沃斤,所以ViewModel層如果想實(shí)現(xiàn)Toast或列表的展示,需要借助MVP模式添加一個(gè)Presenter層挤聘,通過調(diào)用Presenter層來(lái)實(shí)現(xiàn)轰枝。這樣就不再是單純的MVVM模式,而是MVVM+MVP了组去。
優(yōu)點(diǎn):
1. 不明顯
缺點(diǎn):
1. 在布局文件xml中加入了很多邏輯代碼鞍陨,違背了展示和邏輯分離的原則,增加了復(fù)雜度从隆,難以閱讀和維護(hù)
2. 單純的MVVM模式只能實(shí)現(xiàn)簡(jiǎn)單的UI更新诚撵,無(wú)法實(shí)現(xiàn)諸如列表更新的功能,以及加載完成網(wǎng)絡(luò)數(shù)據(jù)后彈一個(gè)toast之類的功能键闺,必須配合MVP添加一個(gè)Presenter實(shí)現(xiàn)
綜上寿烟,我認(rèn)為MVVM理論意義大于實(shí)用意義,而MVP可以適當(dāng)使用以方便代碼維護(hù)的測(cè)試辛燥。
參考:
http://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference
https://developer.android.com/tools/data-binding/guide.html