Android開發模式:MVP Vs MVVM
開發模式
Android常用的開發模式包括MVC,MVP以及MVVM。標準MVC模式不適用于Android的開發,在標準的MVC開發模式中(如網絡請求的服務器開發),action(一個URL請求)首先被Controller接收,Controller讀取Model的數據,生成View并返回。但是在Android中,Activity/Fragment作為交互的起點,代表的是View而不是Controller,單純的套用MVC模式會使得Activity/Fragment中混雜Controller層的代碼,不利于維護和測試。相比之下,MVP和MVVM更易于實現View層和邏輯代碼的分離,本文將通過樣例代碼對MVP和MVVM兩種模式進行講解。
Demo效果
MVP
MVP包括Model,View和Presenter三部分,通過Presenter層將View和Model隔離開。View和Presenter互相持有對方的引用,可以互相調用。Presenter持有Model的引用,可以調用Model的方法,Model可以通過Presenter的回調函數提醒某個事件的結束,如數據加載成功或失敗,交互圖如下圖所示:
MVP交互圖
代碼示例:
代碼結構如下:
代碼結構
在MVP開發模式中,對View的操作都是通過接口(Interface)實現的,對應于Demo中的MvpDemoViewBase:
public interface MvpDemoViewBase {
void updateFirstNameView(String firstName);
void updateLastNameView(String lastName);
void showToastInfo(String toast);
}
該接口定義了三個操作View的函數,updateFirstNameView,updateLastNameView和showToastInfo。
作為View的MvpDemoActivity類實現該接口,提供三個函數的具體實現:
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函數中初始化Presenter:
public class MvpDemoActivity extends AppCompatActivity implements MvpDemoViewBase {
private MvpDemoActivityPresenter mPresenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
mPresenter = new MvpDemoActivityPresenter(this);
}
通過Presenter的引用發起數據請求操作:
@OnClick(R.id.load_button)protected void onClickLoad(View v) {
mPresenter.loadUserData();
}
Presenter持有View和Model的引用,從Model加載數據,并根據返回數據更新View:
public class MvpDemoActivityPresenter implements MvpLoadDataCallBack {
private MvpDemoViewBase view;
private MvpUserModel userModel;
public MvpDemoActivityPresenter(MvpDemoViewBase view) {
this.view = view;
userModel = new MvpUserModel();
}
// 通過Model加載數據
public void loadUserData() {
userModel.loadUserDataFromNet(this);
}
// 加載數據完成后的回調函數
@Override
public void onLoadSuccess() {
// 通過View更新界面
view.updateFirstNameView(userModel.firstName);
view.updateLastNameView(userModel.lastName);
view.showToastInfo("加載成功");
}
@Override
public void onLoadFail() {}
}
Model層實現對數據的定義和加載,并在加載完成后調用Presenter層的回調函數:
public class MvpUserModel {
public String firstName;
public String lastName;
public MvpUserModel() {
this.firstName = "";
this.lastName = "";
}
public void loadUserDataFromNet(MvpLoadDataCallBack callBack) {
// todo: 這里省略了網絡請求的過程
this.firstName = "Jack";
this.lastName = "Wang";
// 請求完成調用Presenter層回調函數,通過Presenter層實現對View的更新
callBack.onLoadSuccess();
}
}
優點:
1. 三層結構比較清晰
2. 可以在沒有View的時候測試Model是否能正常加載數據,只需要寫一個實現了View接口的測試類;同理,可以在沒有Model的時候通過Presenter層fake數據測試View層是否正常;
缺點:
1. 復雜的頁面View層接口可能很多,增加了代碼的數量和維護成本
MVVM
MVVM交互圖
MVVM通過 Data Binding 庫將View的元素和Model的屬性綁定起來,使得Model數據發生變化時對應的View元素自動更新,底層實現是觀察者模式。Data Binding庫是一個Support庫,支持Android 2.1及以上,Gradle版本1.5.0及以上。
學會了Data Binding庫的使用,基本就了解了MVVM的使用。下面通過Demo進行簡單介紹。
代碼結構:
代碼結構
首先在gradle文件中添加如下行啟用Data Binding:
android {
....
dataBinding {
enabled = true
}
}
在布局文件mvvm_demo_layout.xml中添加<data>...</data>段定義數據變量:
<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="加載數據"
android:layout_marginTop="30dp"
android:layout_gravity="center_horizontal"
android:onClick="@{handlers.onClickLoadData}"/>
</LinearLayout>
</layout>
完成布局文件后,Data Binding庫會自動生成一個輔助類MvvmDemoLayoutBind,在MVVMDemoActivity中利用這個輔助類給布局中的變量賦值,并對布局中的元素進行綁定。
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響應事件
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中添加數據的定義和網絡加載過程:
public class MvvmUserModel {
public String firstName;
public String lastName;
public boolean isAdult;
public MvvmUserModel() {
firstName = "";
lastName = "";
isAdult = false;
}
public void loadUserDataFromNet(MvvmLoadDataCallBack callBack) {
// todo: 這里省略了網絡請求的過程
this.firstName = "Jack";
this.lastName = "Wang";
this.isAdult = true;
callBack.onLoadSuccess();
}
}
最后是作為ViewModel層的MvvmUserViewModel類,負責通過Model層的引用調用數據加載過程,并在回調函數中發起更新界面的消息,Data Binding框架會更新跟數據源綁定的View元素,從而實現界面的自動更新。
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模式添加一個Presenter層才能實現
}
@Override
public void onLoadFail() {
}}
這里使用了Bindable注解,通過給指定的函數添加Bindable注解,Data Binding框架會根據函數名自動生成一個BR的屬性,如BR.firstName,在數據源發生變化后,可以調用notifyPropertyChanged(BR.firstName)通知fitstName的變化,getFirstName()返回最新值,更新所有跟firstName數據源綁定的View元素。由于我們需要通過notifyPropertyChanged通知某個或某些數據源的更新,所以MVVM模式中View隨Model的更新而更新并不是完全“自動”完成的,而是需要我們“手動”通知的。
同時,并不是所有的數據展示都能通過Data Binding的方式完成,比如最簡單的展示一個Toast,或者展示一個數據列表。由于ViewModel層并不持有View層的引用,所以ViewModel層如果想實現Toast或列表的展示,需要借助MVP模式添加一個Presenter層,通過調用Presenter層來實現。這樣就不再是單純的MVVM模式,而是MVVM+MVP了。
優點:
1. 不明顯
缺點:
1. 在布局文件xml中加入了很多邏輯代碼,違背了展示和邏輯分離的原則,增加了復雜度,難以閱讀和維護
2. 單純的MVVM模式只能實現簡單的UI更新,無法實現諸如列表更新的功能,以及加載完成網絡數據后彈一個toast之類的功能,必須配合MVP添加一個Presenter實現
綜上,我認為MVVM理論意義大于實用意義,而MVP可以適當使用以方便代碼維護的測試。
參考:
http://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference
https://developer.android.com/tools/data-binding/guide.html
來自:http://www.jianshu.com/p/7ee9bbcb184c