在開始講解各種架構(gòu)模式時(shí),我們先來(lái)看下沒(méi)有經(jīng)過(guò)設(shè)計(jì)的代碼是如何編寫的。為了不分散重點(diǎn),筆者舉的例子會(huì)比較簡(jiǎn)單,初始時(shí)從數(shù)據(jù)庫(kù)緩存中獲取用戶信息展示到界面上,點(diǎn)擊刷新按鈕可以從服務(wù)器上拉取最新的用戶信息并進(jìn)行展示。
由于從數(shù)據(jù)庫(kù)和服務(wù)器上獲取數(shù)據(jù)都屬于更底層的邏輯,因此這兩個(gè)操作一開始就會(huì)進(jìn)行封裝,不會(huì)列入討論范圍,并且為了使程序更加簡(jiǎn)單,這兩個(gè)操作都是使用的測(cè)試代碼進(jìn)行模擬。
User.java
// User實(shí)體類,再?zèng)]有封裝意識(shí)的人,實(shí)體類總會(huì)有一個(gè)吧
public class User {
public String name;
public int age;
}
DbUtils.java
// 數(shù)據(jù)庫(kù)工具
public class DbUtils {
// 查詢數(shù)據(jù)庫(kù)記錄并返回cursor,這里使用測(cè)試代碼直接返回null
public static Cursor query(String sql) {
return null;
}
// 更新數(shù)據(jù)庫(kù)記錄,這里使用測(cè)試代碼不進(jìn)行任何實(shí)際處理
public static void update(String sql) {
}
}
HttpUtils.java
// http工具
public class HttpUtils {
private static Handler sHandler = new Handler(Looper.getMainLooper());
public interface ResponseCallback {
void onResponseSuccessed(String json);
void onResponseFailed(int reason);
}
// 發(fā)起http請(qǐng)求,這里使用模擬的數(shù)據(jù),并有一定機(jī)率請(qǐng)求失敗
public static void request(Map params, final ResponseCallback callback) {
sHandler.postDelayed(new Runnable() {
@Override
public void run() {
int value = new Random(System.currentTimeMillis()).nextInt(5);
if(callback != null) {
if(value == 2) {
callback.onResponseFailed(1);
} else {
callback.onResponseSuccessed("{\"name\": \"純爺們\", \"age\": 20}");
}
}
}
}, 500);
}
}
activity_user.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tv_name"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:id="@+id/tv_age"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:text="刷新"
android:id="@+id/btn_refresh"/>
</LinearLayout>
UserActivity.java
public class UserActivity extends Activity {
private TextView mNameView;
private TextView mAgeView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
// 加載緩存的用戶數(shù)據(jù)并展示
User user = loadUser();
if(user != null) {
mNameView.setText("昵稱:" + user.name);
mAgeView.setText("年齡:" + user.age);
}
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
refresh();
}
});
}
private User loadUser() {
// 這里本應(yīng)從cursor中獲取數(shù)據(jù),但為求程序盡量簡(jiǎn)單,我們直接使用模擬數(shù)據(jù)。之所以要加入query這段代碼,是為了盡可能模擬真實(shí)的流程。
Cursor cursor = DbUtils.query(null);
if(cursor != null) {
try {
} catch (Exception e) {
} finally {
cursor.close();
}
}
User user = new User();
user.name = "小蝦米";
user.age = 19;
return user;
}
private void refresh() {
HttpUtils.request(null, new HttpUtils.ResponseCallback() {
@Override
public void onResponseSuccessed(String json) {
try {
User user = new Gson().fromJson(json, User.class);
// 用戶信息更新了,要同步更新數(shù)據(jù)庫(kù)中的記錄
String updateSql = null;
DbUtils.update(updateSql);
mNameView.setText("昵稱:" + user.name);
mAgeView.setText("年齡:" + user.age);
} catch (Exception e) {
}
}
@Override
public void onResponseFailed(int reason) {
Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
}
});
}
}
點(diǎn)擊刷新前顯示如下界面
點(diǎn)擊
刷新后顯示如下界面上面的例子請(qǐng)讀者務(wù)必記勞,后續(xù)講到的幾種架構(gòu)模式全部使用的都是這個(gè)例子。
上述例子一個(gè)非常突出的問(wèn)題是,用戶信息可能在多個(gè)界面上都需要顯示,而在這些界面上,從數(shù)據(jù)庫(kù)和服務(wù)器上獲取用戶信息的流程都要寫一遍,重復(fù)編寫不僅容易出錯(cuò)也不容易維護(hù)。解決該問(wèn)題的方法就是封裝一個(gè)可復(fù)用的Model,MXX模式也由此產(chǎn)生。
MVC
MVC由Model、View、Controller組成,Android提供的xml布局是View層的主要部分,View本身已經(jīng)是相對(duì)比較獨(dú)立的了,因此MVC中主要考慮的就是Model的設(shè)計(jì)。我們來(lái)看下MVC中各層在Android中的主要應(yīng)用:
- Model:表示模型層,模型的一個(gè)核心特點(diǎn)就是可復(fù)用。包含數(shù)據(jù)業(yè)務(wù)實(shí)體(entity/bean)本身,以及圍繞該實(shí)體進(jìn)行的業(yè)務(wù)操作(本地或遠(yuǎn)程增刪改查操作)。即Model層主要針對(duì)數(shù)據(jù),包含數(shù)據(jù)實(shí)體和數(shù)據(jù)訪問(wèn),如果非要以模型來(lái)稱呼和理解的話,前者為數(shù)據(jù)模型,后者為業(yè)務(wù)模型。
- View:表示視圖層,負(fù)責(zé)界面數(shù)據(jù)的展示,以及響應(yīng)用戶操作。
- Controller:表示控制層,負(fù)責(zé)邏輯處理,由其連接Model和View。Controller通過(guò)Model獲取數(shù)據(jù)并傳遞給View進(jìn)行展示;通過(guò)響應(yīng)View傳遞過(guò)來(lái)的用戶事件調(diào)用Model的接口進(jìn)行業(yè)務(wù)處理。
后續(xù)內(nèi)容都使用簡(jiǎn)稱,M代表Model,V代表View,C代表Controller。
其中,V和C一般又統(tǒng)稱為UI層,由于Android已經(jīng)提供了xml布局,因此在Android中V和C并不需要刻意區(qū)分,可以統(tǒng)一以UI層來(lái)理解,UI層的核心代碼包含xml和Activity(或Fragment,或另外封裝的控制器),后者既扮演著部分V的角色,又扮演著全部的C角色。我們來(lái)看下使用MVC重構(gòu)后的例子,增加了UserBusiness類,修改了UserActivity的代碼,以下只貼出更新的部分代碼,其余代碼請(qǐng)參考之前的例子。
UserBusiness.java
public class UserBusiness {
private static final UserBusiness INSTANCE = new UserBusiness();
private List<UserListener> mListeners = new LinkedList<>();
public static UserBusiness get() {
return INSTANCE;
}
public void addListener(UserListener listener) {
if(listener == null) {
return;
}
synchronized (mListeners) {
if(!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
}
public void removeListener(UserListener listener) {
if(listener == null) {
return;
}
synchronized (mListeners) {
mListeners.remove(listener);
}
}
public User getUser() {
Cursor cursor = DbUtils.query(null);
if(cursor != null) {
try {
} catch (Exception e) {
} finally {
cursor.close();
}
}
User user = new User();
user.name = "小蝦米";
user.age = 19;
return user;
}
public void requestUser() {
HttpUtils.request(null, new HttpUtils.ResponseCallback() {
@Override
public void onResponseSuccessed(String json) {
User user = null;
try {
user = new Gson().fromJson(json, User.class);
} catch (Exception e) {
}
if(user != null) {
String updateSql = null;
DbUtils.update(updateSql);
notifyRequestUser(0, user);
} else {
notifyRequestUser(1, null);
}
}
@Override
public void onResponseFailed(int reason) {
notifyRequestUser(reason, null);
}
});
}
private void notifyRequestUser(int code, User user) {
List<UserListener> listeners = new LinkedList<>();
synchronized (mListeners) {
listeners.addAll(mListeners);
}
for(UserListener listener : listeners) {
listener.onRequestUserResult(code, user);
}
}
public interface UserListener {
void onRequestUserResult(int code, User user);
}
}
UserActivity.java
public class UserActivity extends Activity implements UserBusiness.UserListener {
private TextView mNameView;
private TextView mAgeView;
private UserBusiness mUserBusiness = UserBusiness.get();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
// 加載緩存的用戶數(shù)據(jù)并展示
User user = mUserBusiness.getUser();
if(user != null) {
mNameView.setText("昵稱:" + user.name);
mAgeView.setText("年齡:" + user.age);
}
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
mUserBusiness.requestUser();
}
});
mUserBusiness.addListener(this);
}
@Override
protected void onDestroy() {
mUserBusiness.removeListener(this);
super.onDestroy();
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mNameView.setText("昵稱:" + user.name);
mAgeView.setText("年齡:" + user.age);
} else {
Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
}
}
}
重構(gòu)后的代碼有如下優(yōu)點(diǎn):
- Activity的代碼變得簡(jiǎn)單和整潔了,Activity現(xiàn)在只需要處理控制邏輯(UI邏輯),以及作為V和M通信的橋梁。
- 業(yè)務(wù)代碼封裝在
UserBusiness中,一是隱藏了數(shù)據(jù)操作(業(yè)務(wù)流程)的具體細(xì)節(jié),使得UI層在訪問(wèn)時(shí)更簡(jiǎn)單了;二是可以復(fù)用,任何模塊都可以輕松訪問(wèn),且可以通過(guò)在UserBusiness中注冊(cè)一個(gè)監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)用戶業(yè)務(wù)的相關(guān)事件。
但MVC仍具有以下缺點(diǎn):
- V不可復(fù)用,然而在Android中V復(fù)用沒(méi)有意義,需要復(fù)用的話完全可以封裝可復(fù)用的控件,然后V組裝這些控件。
- C不可復(fù)用。
- V和C之間還存在部分耦合,因此除了M外V和C都無(wú)法進(jìn)行單元測(cè)試。
為了解決以上缺點(diǎn),便有了MVP。
MVP
MVP由Model、View和Presenter組成,M和V就不再重復(fù)解釋了,P和C一樣,承擔(dān)著控制層的責(zé)任。MVP相比MVC作了如下改進(jìn)(或者說(shuō)變化,是否改進(jìn)視情況而定):
- P可復(fù)用,這意味著不能再使用Activity(或...)作為P了,很簡(jiǎn)單,Activity不能復(fù)用(使用繼承達(dá)到復(fù)用的場(chǎng)景不在這討論范圍之內(nèi))。由此,Activity從控制層的角色轉(zhuǎn)向了視圖層,即在MVP中V由xml和Activity組成。
也可以另外抽離一個(gè)V,然后將Activity作為創(chuàng)建V和P的管理器,并負(fù)責(zé)將V和P進(jìn)行綁定。但不建議采用這種方式,額外增加了代碼,并且也沒(méi)帶來(lái)多少益處,除非想要復(fù)用V或者界面異常復(fù)雜而拆分了多個(gè)V和P。
- MVP三者皆可以獨(dú)立完成單元測(cè)試,為了達(dá)到這個(gè)目的,P和V需要做到完全解耦,解耦一般使用接口。
我們來(lái)看下使用MVP重構(gòu)過(guò)的代碼,在mvc的基礎(chǔ)上主要是改動(dòng)了UserActivity.java,然后增加了幾個(gè)類。
PresenterContext.java
public interface PresenterContext {
Activity getActivity();
}
UserPresenter.java
public interface UserPresenter {
void onRefresh();
void onInited();
void onDestroyed();
}
UserPresenterImpl.java
public class UserPresenterImpl implements UserPresenter, UserBusiness.UserListener {
private PresenterContext mContext;
private UserView mView;
private UserBusiness mUserBusiness = UserBusiness.get();
public UserPresenterImpl(PresenterContext context, UserView view) {
mContext = context;
mView = view;
}
@Override
public void onRefresh() {
mUserBusiness.requestUser();
}
@Override
public void onInited() {
mUserBusiness.addListener(this);
User user = mUserBusiness.getUser();
if(user != null) {
mView.updateName(user.name);
mView.updateAge(user.age);
}
}
@Override
public void onDestroyed() {
mUserBusiness.removeListener(this);
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mView.updateName(user.name);
mView.updateAge(user.age);
} else {
Toast.makeText(mContext.getActivity(), "刷新失敗", Toast.LENGTH_SHORT).show();
}
}
}
UserView.java
public interface UserView {
void updateName(String name);
void updateAge(int age);
}
UserActivity.java
public class UserActivity extends Activity implements UserView, PresenterContext {
private TextView mNameView;
private TextView mAgeView;
private UserPresenter mPresenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
mPresenter = new UserPresenterImpl(this, this);
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.onRefresh();
}
});
mPresenter.onInited();
}
@Override
protected void onDestroy() {
mPresenter.onDestroyed();
super.onDestroy();
}
@Override
public void updateName(String name) {
mNameView.setText("昵稱:" + name);
}
@Override
public void updateAge(int age) {
mAgeView.setText("年齡:" + age);
}
@Override
public Activity getActivity() {
return this;
}
}
我們來(lái)講解下:
- 由于Presenter不像Activity一樣,它沒(méi)有上下文信息,因此增加了
PresenterContext類作為Presenter的上下文信息。這里
PresenterContext只是用來(lái)獲取Activity,是因?yàn)槭纠橇η蠛?jiǎn)單,實(shí)際項(xiàng)目中它可以獲取的信息會(huì)更多。 -
UserActivity的代碼被拆分成了兩部分,一部分仍然在UserActivity中,作為View的代碼,一部分抽離到UserPresenterImpl中,作為Presenter的代碼。至此,將視圖和控制層的代碼完全分離了。 - 新建了
UserView和UserPresenter兩個(gè)接口用來(lái)表示V和P,V的實(shí)現(xiàn)方UserActivity持有UserPresenter接口而非具體的實(shí)現(xiàn)類,P的實(shí)現(xiàn)方持有UserView接口而非具體的實(shí)現(xiàn)類,從而達(dá)到V和P解耦。需要對(duì)P進(jìn)行單元測(cè)試時(shí),只需要?jiǎng)?chuàng)建一個(gè)類簡(jiǎn)單地實(shí)現(xiàn)
UserView的類,然后和UserPresenterImpl綁定即可;需要對(duì)V進(jìn)行單元測(cè)試時(shí),只需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單實(shí)現(xiàn)UserPresenter的類,然后在UserActivity中將構(gòu)建P的那行代碼修改下即可。
MVP相對(duì)MVC具有P復(fù)用及方便做單元測(cè)試的優(yōu)點(diǎn),然而,在實(shí)際Android項(xiàng)目中,P復(fù)用的場(chǎng)景基本不存在,且多數(shù)公司并沒(méi)有做單元測(cè)試。因此,多數(shù)情況下,MVC可能比MVP更適合Android項(xiàng)目,畢竟MVP多引入了不入類和代碼,且?guī)?lái)解耦的同時(shí)也使得代碼更加“繞”。
MVVM
不管是MVC還是MVP都存在幾下問(wèn)題:
- 在每個(gè)界面都要編寫不少的
findViewById、setOnClickListener之類的代碼。 - 數(shù)據(jù)更新后,要手動(dòng)調(diào)用
setText之類的代碼刷新視圖。
以上幾點(diǎn)都不是什么大問(wèn)題,編寫這些代碼也不會(huì)輕易出錯(cuò),但總有達(dá)人追求極致,MVVM便由此產(chǎn)生了。Android解決第問(wèn)題1的方法是在xml中直接嵌入代碼(類似JSX的寫法),解決問(wèn)題2的方法是提供了DataBinding方案綁定視圖和數(shù)據(jù)。MVVM真正地將V層完全地體現(xiàn)在xml上,M層還是老樣子(模式怎么變它都不變),VM(ViewModel)用來(lái)代替C和P,以MVC作為改造,VM包括Activity和DataBinding(自動(dòng)生成)。
本文主要講解幾種架構(gòu)模式的應(yīng)用場(chǎng)景和區(qū)別,因此不會(huì)過(guò)多講解MVVM在Android中如何使用,沒(méi)接觸過(guò)MVVM的建議先看下這篇入門文章或查閱官方文檔。
使用DataBinding需要在build.gradle中加入如下代碼:
android {
dataBinding {
enabled = true
}
}
配置了之后build時(shí)會(huì)下載DataBinding的依賴包以及自動(dòng)生成部分代碼,自動(dòng)生成的代碼后續(xù)遇到時(shí)會(huì)提到。
接著我們來(lái)看下在MVC的基礎(chǔ)上變更后的MVVM代碼。
activity_user.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.sean.mvvm.model.entity.User" />
<variable
name="host"
type="com.sean.mvvm.UserActivity"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text='@{"昵稱:" + user.name}' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text='@{"年齡:" + String.valueOf(user.age)}'/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:text="刷新"
android:onClick="@{host.onRefresh}"/>
</LinearLayout>
</layout>
User.java
public class User extends BaseObservable {
@Bindable
public String name;
@Bindable
public int age;
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}
UserActivity.java
public class UserActivity extends Activity implements UserBusiness.UserListener {
private User mUser;
private UserBusiness mUserBusiness = UserBusiness.get();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
mUser = mUserBusiness.getUser();
if(mUser == null) {
mUser = new User();
}
binding.setUser(mUser);
binding.setHost(this);
mUserBusiness.addListener(this);
}
@Override
protected void onDestroy() {
mUserBusiness.removeListener(this);
super.onDestroy();
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mUser.setName(user.name);
mUser.setAge(user.age);
} else {
Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
}
}
public void onRefresh(View v) {
mUserBusiness.requestUser();
}
}
分析一下:
- xml布局的頂部標(biāo)簽變成了
layout,data標(biāo)簽存儲(chǔ)非布局相關(guān)代碼,variable定義變量。 - 編譯時(shí)會(huì)根據(jù)xml名稱和內(nèi)容自動(dòng)生成binding類,如例子中的
ActivityUserBinding,variable定義的變量在binding中都有對(duì)應(yīng)的set、get方法。 - 數(shù)據(jù)改變時(shí)要做到自動(dòng)刷新UI,實(shí)體類必須繼承
BaseObservable,且需要自動(dòng)觸發(fā)的字段必須以Bindable注解,并在字段值變化時(shí)調(diào)用notifyPropertyChanged方法。
PS:強(qiáng)烈建議至少熟讀一個(gè)自動(dòng)生成的binding類,綁定的所有原理都在這里,代碼很容易理解,也不需要去網(wǎng)絡(luò)上尋求答案。
上面的例子實(shí)現(xiàn)了數(shù)據(jù)的單向綁定(數(shù)據(jù)更新觸發(fā)UI更新)和事件的綁定,而Android是支持?jǐn)?shù)據(jù)雙向綁定的,現(xiàn)在來(lái)看下當(dāng)UI更新時(shí)如何觸發(fā)數(shù)據(jù)的更新。使用方式其實(shí)很簡(jiǎn)單,在xml中小小修改下就行:
android:text='@={user.name}'
注意到@后面多了個(gè)=,同時(shí)昵稱:去掉了,=表示數(shù)據(jù)雙向綁定,但當(dāng)=存在時(shí),右側(cè)的表達(dá)式只能是個(gè)變量,因此昵稱:只能去掉了。當(dāng)xml改成這樣后,TextView的文本發(fā)生變化了,user.name的值也會(huì)隨之更新。
總結(jié)下MVVM的優(yōu)缺點(diǎn):
- 新型xml更加成熟,可以獨(dú)立支撐View層。然而,這可能也是缺點(diǎn),畢竟這種xml編程方式和Android傳統(tǒng)的方式差異較大。
如果不想改變xml的編寫方式,又希望使用MVVM,那么可以仿照自動(dòng)生成的binding類自己編寫一個(gè)ViewModel,但是有沒(méi)有這個(gè)必要呢。。。
- 只關(guān)心數(shù)據(jù)變化,而不需要關(guān)注視圖的刷新,刷新由自動(dòng)生成的binding處理了。
- 數(shù)據(jù)雙向綁定是個(gè)偽命題,實(shí)際上并沒(méi)有完全做到自動(dòng)化,還是需要手動(dòng)編寫額外的代碼,并且也有條件限制。
- 在非主module(一般為app)中無(wú)法編譯新型xml(這個(gè)也可能是筆者使用不當(dāng),有待確認(rèn))。
綜上,MVVM相比MVC、MVP并沒(méi)有多大優(yōu)勢(shì),但可以通知配置減少一些重復(fù)的邏輯代碼。使用哪種模式根據(jù)實(shí)際情況而定,沒(méi)有誰(shuí)比誰(shuí)更好。