在開始講解各種架構(gòu)模式時,我們先來看下沒有經(jīng)過設(shè)計的代碼是如何編寫的讥蟆。為了不分散重點勒虾,筆者舉的例子會比較簡單,初始時從數(shù)據(jù)庫緩存中獲取用戶信息展示到界面上攻询,點擊刷新按鈕可以從服務(wù)器上拉取最新的用戶信息并進(jìn)行展示从撼。
由于從數(shù)據(jù)庫和服務(wù)器上獲取數(shù)據(jù)都屬于更底層的邏輯,因此這兩個操作一開始就會進(jìn)行封裝,不會列入討論范圍低零,并且為了使程序更加簡單婆翔,這兩個操作都是使用的測試代碼進(jìn)行模擬。
User.java
// User實體類掏婶,再沒有封裝意識的人啃奴,實體類總會有一個吧
public class User {
public String name;
public int age;
}
DbUtils.java
// 數(shù)據(jù)庫工具
public class DbUtils {
// 查詢數(shù)據(jù)庫記錄并返回cursor,這里使用測試代碼直接返回null
public static Cursor query(String sql) {
return null;
}
// 更新數(shù)據(jù)庫記錄雄妥,這里使用測試代碼不進(jìn)行任何實際處理
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請求最蕾,這里使用模擬的數(shù)據(jù),并有一定機(jī)率請求失敗
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ù)老厌,但為求程序盡量簡單瘟则,我們直接使用模擬數(shù)據(jù)。之所以要加入query這段代碼枝秤,是為了盡可能模擬真實的流程醋拧。
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ù)庫中的記錄
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();
}
});
}
}
點擊刷新
前顯示如下界面
點擊
刷新
后顯示如下界面上面的例子請讀者務(wù)必記勞淀弹,后續(xù)講到的幾種架構(gòu)模式全部使用的都是這個例子丹壕。
上述例子一個非常突出的問題是,用戶信息可能在多個界面上都需要顯示薇溃,而在這些界面上菌赖,從數(shù)據(jù)庫和服務(wù)器上獲取用戶信息的流程都要寫一遍,重復(fù)編寫不僅容易出錯也不容易維護(hù)沐序。解決該問題的方法就是封裝一個可復(fù)用的Model琉用,MXX模式也由此產(chǎn)生。
MVC
MVC由Model策幼、View辕羽、Controller組成,Android提供的xml布局是View層的主要部分垄惧,View本身已經(jīng)是相對比較獨立的了,因此MVC中主要考慮的就是Model的設(shè)計绰寞。我們來看下MVC中各層在Android中的主要應(yīng)用:
- Model:表示模型層到逊,模型的一個核心特點就是可復(fù)用。包含數(shù)據(jù)業(yè)務(wù)實體(entity/bean)本身滤钱,以及圍繞該實體進(jìn)行的業(yè)務(wù)操作(本地或遠(yuǎn)程增刪改查操作)觉壶。即Model層主要針對數(shù)據(jù),包含數(shù)據(jù)實體和數(shù)據(jù)訪問件缸,如果非要以模型來稱呼和理解的話铜靶,前者為數(shù)據(jù)模型,后者為業(yè)務(wù)模型他炊。
- View:表示視圖層争剿,負(fù)責(zé)界面數(shù)據(jù)的展示已艰,以及響應(yīng)用戶操作。
- Controller:表示控制層蚕苇,負(fù)責(zé)邏輯處理哩掺,由其連接Model和View。Controller通過Model獲取數(shù)據(jù)并傳遞給View進(jìn)行展示涩笤;通過響應(yīng)View傳遞過來的用戶事件調(diào)用Model的接口進(jìn)行業(yè)務(wù)處理嚼吞。
后續(xù)內(nèi)容都使用簡稱,M代表Model蹬碧,V代表View舱禽,C代表Controller。
其中恩沽,V和C一般又統(tǒng)稱為UI層誊稚,由于Android已經(jīng)提供了xml布局,因此在Android中V和C并不需要刻意區(qū)分飒筑,可以統(tǒng)一以UI層來理解片吊,UI層的核心代碼包含xml和Activity(或Fragment,或另外封裝的控制器)协屡,后者既扮演著部分V的角色俏脊,又扮演著全部的C角色。我們來看下使用MVC重構(gòu)后的例子肤晓,增加了UserBusiness
類爷贫,修改了UserActivity
的代碼,以下只貼出更新的部分代碼补憾,其余代碼請參考之前的例子漫萄。
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)點:
- Activity的代碼變得簡單和整潔了,Activity現(xiàn)在只需要處理控制邏輯(UI邏輯)盈匾,以及作為V和M通信的橋梁腾务。
- 業(yè)務(wù)代碼封裝在
UserBusiness
中,一是隱藏了數(shù)據(jù)操作(業(yè)務(wù)流程)的具體細(xì)節(jié)削饵,使得UI層在訪問時更簡單了岩瘦;二是可以復(fù)用,任何模塊都可以輕松訪問窿撬,且可以通過在UserBusiness
中注冊一個監(jiān)聽器來監(jiān)聽用戶業(yè)務(wù)的相關(guān)事件启昧。
但MVC仍具有以下缺點:
- V不可復(fù)用,然而在Android中V復(fù)用沒有意義劈伴,需要復(fù)用的話完全可以封裝可復(fù)用的控件密末,然后V組裝這些控件。
- C不可復(fù)用。
- V和C之間還存在部分耦合严里,因此除了M外V和C都無法進(jìn)行單元測試新啼。
為了解決以上缺點,便有了MVP田炭。
MVP
MVP由Model师抄、View和Presenter組成,M和V就不再重復(fù)解釋了教硫,P和C一樣叨吮,承擔(dān)著控制層的責(zé)任。MVP相比MVC作了如下改進(jìn)(或者說變化瞬矩,是否改進(jìn)視情況而定):
- P可復(fù)用茶鉴,這意味著不能再使用Activity(或...)作為P了,很簡單景用,Activity不能復(fù)用(使用繼承達(dá)到復(fù)用的場景不在這討論范圍之內(nèi))涵叮。由此,Activity從控制層的角色轉(zhuǎn)向了視圖層伞插,即在MVP中V由xml和Activity組成割粮。
也可以另外抽離一個V,然后將Activity作為創(chuàng)建V和P的管理器媚污,并負(fù)責(zé)將V和P進(jìn)行綁定舀瓢。但不建議采用這種方式,額外增加了代碼耗美,并且也沒帶來多少益處京髓,除非想要復(fù)用V或者界面異常復(fù)雜而拆分了多個V和P。
- MVP三者皆可以獨立完成單元測試商架,為了達(dá)到這個目的堰怨,P和V需要做到完全解耦,解耦一般使用接口蛇摸。
我們來看下使用MVP重構(gòu)過的代碼备图,在mvc的基礎(chǔ)上主要是改動了UserActivity.java
,然后增加了幾個類赶袄。
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;
}
}
我們來講解下:
- 由于Presenter不像Activity一樣诬烹,它沒有上下文信息,因此增加了
PresenterContext
類作為Presenter的上下文信息弃鸦。這里
PresenterContext
只是用來獲取Activity,是因為示例是力求簡單幢痘,實際項目中它可以獲取的信息會更多唬格。 -
UserActivity
的代碼被拆分成了兩部分,一部分仍然在UserActivity
中,作為View的代碼购岗,一部分抽離到UserPresenterImpl
中汰聋,作為Presenter的代碼。至此喊积,將視圖和控制層的代碼完全分離了烹困。 - 新建了
UserView
和UserPresenter
兩個接口用來表示V和P,V的實現(xiàn)方UserActivity
持有UserPresenter
接口而非具體的實現(xiàn)類乾吻,P的實現(xiàn)方持有UserView
接口而非具體的實現(xiàn)類髓梅,從而達(dá)到V和P解耦。需要對P進(jìn)行單元測試時绎签,只需要創(chuàng)建一個類簡單地實現(xiàn)
UserView
的類枯饿,然后和UserPresenterImpl
綁定即可;需要對V進(jìn)行單元測試時诡必,只需要創(chuàng)建一個簡單實現(xiàn)UserPresenter
的類奢方,然后在UserActivity
中將構(gòu)建P的那行代碼修改下即可。
MVP相對MVC具有P復(fù)用及方便做單元測試的優(yōu)點爸舒,然而蟋字,在實際Android項目中,P復(fù)用的場景基本不存在扭勉,且多數(shù)公司并沒有做單元測試鹊奖。因此,多數(shù)情況下剖效,MVC可能比MVP更適合Android項目嫉入,畢竟MVP多引入了不入類和代碼,且?guī)斫怦畹耐瑫r也使得代碼更加“繞”璧尸。
MVVM
不管是MVC還是MVP都存在幾下問題:
- 在每個界面都要編寫不少的
findViewById
咒林、setOnClickListener
之類的代碼。 - 數(shù)據(jù)更新后爷光,要手動調(diào)用
setText
之類的代碼刷新視圖垫竞。
以上幾點都不是什么大問題,編寫這些代碼也不會輕易出錯蛀序,但總有達(dá)人追求極致欢瞪,MVVM便由此產(chǎn)生了。Android解決第問題1的方法是在xml中直接嵌入代碼(類似JSX的寫法)徐裸,解決問題2的方法是提供了DataBinding
方案綁定視圖和數(shù)據(jù)陨瘩。MVVM真正地將V層完全地體現(xiàn)在xml上,M層還是老樣子(模式怎么變它都不變)埠通,VM(ViewModel)用來代替C和P声怔,以MVC作為改造回懦,VM包括Activity
和DataBinding
(自動生成)。
本文主要講解幾種架構(gòu)模式的應(yīng)用場景和區(qū)別次企,因此不會過多講解MVVM在Android中如何使用怯晕,沒接觸過MVVM的建議先看下這篇入門文章或查閱官方文檔。
使用DataBinding
需要在build.gradle中加入如下代碼:
android {
dataBinding {
enabled = true
}
}
配置了之后build時會下載DataBinding
的依賴包以及自動生成部分代碼缸棵,自動生成的代碼后續(xù)遇到時會提到舟茶。
接著我們來看下在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)簽存儲非布局相關(guān)代碼吧凉,variable
定義變量。 - 編譯時會根據(jù)xml名稱和內(nèi)容自動生成binding類型诚,如例子中的
ActivityUserBinding
客燕,variable
定義的變量在binding中都有對應(yīng)的set、get方法狰贯。 - 數(shù)據(jù)改變時要做到自動刷新UI也搓,實體類必須繼承
BaseObservable
,且需要自動觸發(fā)的字段必須以Bindable
注解涵紊,并在字段值變化時調(diào)用notifyPropertyChanged
方法傍妒。
PS:強(qiáng)烈建議至少熟讀一個自動生成的binding類,綁定的所有原理都在這里摸柄,代碼很容易理解颤练,也不需要去網(wǎng)絡(luò)上尋求答案。
上面的例子實現(xiàn)了數(shù)據(jù)的單向綁定(數(shù)據(jù)更新觸發(fā)UI更新)和事件的綁定驱负,而Android是支持?jǐn)?shù)據(jù)雙向綁定的嗦玖,現(xiàn)在來看下當(dāng)UI更新時如何觸發(fā)數(shù)據(jù)的更新。使用方式其實很簡單跃脊,在xml中小小修改下就行:
android:text='@={user.name}'
注意到@
后面多了個=
宇挫,同時昵稱:
去掉了,=
表示數(shù)據(jù)雙向綁定酪术,但當(dāng)=
存在時器瘪,右側(cè)的表達(dá)式只能是個變量,因此昵稱:
只能去掉了绘雁。當(dāng)xml改成這樣后橡疼,TextView的文本發(fā)生變化了,user.name
的值也會隨之更新庐舟。
總結(jié)下MVVM的優(yōu)缺點:
- 新型xml更加成熟欣除,可以獨立支撐View層。然而挪略,這可能也是缺點耻涛,畢竟這種xml編程方式和Android傳統(tǒng)的方式差異較大废酷。
如果不想改變xml的編寫方式,又希望使用MVVM抹缕,那么可以仿照自動生成的binding類自己編寫一個ViewModel,但是有沒有這個必要呢墨辛。卓研。。
- 只關(guān)心數(shù)據(jù)變化睹簇,而不需要關(guān)注視圖的刷新奏赘,刷新由自動生成的binding處理了。
- 數(shù)據(jù)雙向綁定是個偽命題太惠,實際上并沒有完全做到自動化磨淌,還是需要手動編寫額外的代碼,并且也有條件限制凿渊。
- 在非主module(一般為app)中無法編譯新型xml(這個也可能是筆者使用不當(dāng)梁只,有待確認(rèn))。
綜上埃脏,MVVM相比MVC搪锣、MVP并沒有多大優(yōu)勢,但可以通知配置減少一些重復(fù)的邏輯代碼彩掐。使用哪種模式根據(jù)實際情況而定构舟,沒有誰比誰更好。