前面三篇介紹了Jetpack 架構組件中 最重要 的部分:生命周期組件-Lifecycle喇辽、感知生命周期的數(shù)據(jù)組件-LiveData掌挚、視圖模型組件-ViewModel。 這篇菩咨,就來探索下目前android開發(fā)中 最優(yōu)秀吠式、討論最多的架構模式—— MVVM 。
幾個月前抽米,我所在項目完成了 MVVM 的架構改造特占。這篇在開始寫之前,我也閱讀了大量MVVM文章云茸。所以是目,這篇盡量講清楚 開發(fā)架構模式和MVVM的本質,使得有一種 “哦标捺,原來如此” 的豁然開朗懊纳。
注意,本篇完全 不會提 DataBinding亡容、雙向綁定嗤疯,文末會解釋為啥不提。
一闺兢、開發(fā)架構 是什么茂缚?
我們先來理解開發(fā)架構的本質是什么,維基百科對軟件架構的描述如下:
軟件架構是一個系統(tǒng)的草圖屋谭。軟件架構描述的對象是直接構成系統(tǒng)的抽象組件脚囊。各個組件之間的連接則明確和相對細致地描述組件之間的通訊。在實現(xiàn)階段桐磁,這些抽象組件被細化為實際的組件凑术,比如具體某個類或者對象。在面向對象領域中所意,組件之間的連接通常用接口來實現(xiàn)。
拆分開來就是三條:
- 針對的是一個完整系統(tǒng)催首,此系統(tǒng)可以實現(xiàn)某種功能扶踊。
- 系統(tǒng)包含多個模塊,模塊間有一些關系和連接郎任。
- 架構是實現(xiàn)此系統(tǒng)的實施描述:模塊責任秧耗、模塊間的連接。
為啥要做開發(fā)架構設計呢舶治?
- 模塊化責任具體化分井,使得每個模塊專注自己內部车猬。
- 模塊間的關聯(lián)簡單化,減少耦合尺锚。
- 易于使用珠闰、維護性好
- 提高開發(fā)效率
架構模式最終都是 服務于開發(fā)者。如果代碼職責和邏輯混亂瘫辩,維護成本就會相應地上升伏嗜。
宏觀上來說,開發(fā)架構是一種思想伐厌,每個領域都有一些成熟的架構模式承绸,選擇適合自己項目即可。
二挣轨、Android開發(fā)中的架構
具體到Android開發(fā)中军熏,開發(fā)架構就是描述 視圖層、邏輯層卷扮、數(shù)據(jù)層 三者之間的關系和實施:
- 視圖層:用戶界面荡澎,即界面的展示、以及交互事件的響應画饥。
- 邏輯層:為了實現(xiàn)系統(tǒng)功能而進行的必要邏輯衔瓮。
- 數(shù)據(jù)層:數(shù)據(jù)的獲取和存儲,含本地抖甘、server热鞍。
正常的開發(fā)流程中,開始寫代碼之前 都會有架構設計這一過程衔彻。這就需要你選擇使用何種架構模式了薇宠。
我的Android開發(fā)之路完整地經過了 MVC、MVP艰额、MVVM澄港,相信很多開發(fā)者和我一樣都是這樣一個過程,先來回顧下三者柄沮。
2.1 MVC
MVC回梧,Model-View-Controller,職責分類如下:
- Model祖搓,模型層狱意,即數(shù)據(jù)模型,用于獲取和存儲數(shù)據(jù)拯欧。
- View详囤,視圖層,即xml布局
- Controller镐作,控制層藏姐,負責業(yè)務邏輯隆箩。
View層 接收到用戶操作事件,通知到 Controller 進行對應的邏輯處理羔杨,然后通知 Model去獲取/更新數(shù)據(jù)捌臊,Model 再把新的數(shù)據(jù) 通知到 View 更新界面。這就是一個完整 MVC 的數(shù)據(jù)流向问畅。
但在Android中娃属,因為xml布局能力很弱,View的很多操作是在Activity/Fragment中的护姆,而業(yè)務邏輯同樣也是寫在Activity/Fragment中矾端。
所以,MVC 的問題點 如下:
- Activity/Fragment 責任不明卵皂,同時負責View秩铆、Controller,就會導致其中代碼量大灯变,不滿足單一職責殴玛。
- Model耦合View,View 的修改會導致 Controller 和 Model 都進行改動添祸,不滿足最少知識原則滚粟。
2.2 MVP
MVP,Model-View-Presenter刃泌,職責分類如下:
- Model凡壤,模型層,即數(shù)據(jù)模型耙替,用于獲取和存儲數(shù)據(jù)亚侠。
- View,視圖層俗扇,即Activity/Fragment
- Presenter硝烂,控制層,負責業(yè)務邏輯铜幽。
MVP解決了MVC的問題:1.View責任明確滞谢,邏輯不再寫在Activity中,而是在Presenter中除抛;2.Model不再持有View爹凹。
View層 接收到用戶操作事件,通知到Presenter镶殷,Presenter進行邏輯處理,然后通知Model更新數(shù)據(jù)微酬,Model 把更新的數(shù)據(jù)給到Presenter绘趋,Presenter再通知到 View 更新界面颤陶。
MVP的實現(xiàn)思路:
- UI邏輯抽象成IView接口,由具體的Activity實現(xiàn)類來完成陷遮。且調用Presenter進行邏輯操作滓走。
- 業(yè)務邏輯抽象成IPresenter接口,由具體的Presenter實現(xiàn)類來完成帽馋。邏輯操作完成后調用IView接口方法刷新UI搅方。
MVP 本質是面向接口編程,實現(xiàn)了依賴倒置原則绽族。MVP解決了View層責任不明的問題姨涡,但并沒有解決代碼耦合的問題,View和Presenter之間相互持有吧慢。
所以 MVP 有問題點 如下:
- 會引入大量的IView涛漂、IPresenter接口,增加實現(xiàn)的復雜度检诗。
- View和Presenter相互持有匈仗,形成耦合。
2.3 MVVM
MVVM逢慌,Model-View-ViewModel悠轩,職責分類如下:
- Model,模型層攻泼,即數(shù)據(jù)模型火架,用于獲取和存儲數(shù)據(jù)。
- View坠韩,視圖距潘,即Activity/Fragment
- ViewModel,視圖模型只搁,負責業(yè)務邏輯音比。
注意,MVVM這里的ViewModel就是一個名稱氢惋,可以理解為MVP中的Presenter洞翩。不等同于上一篇中的 ViewModel組件 ,Jetpack ViewModel組件是 對 MVVM的ViewModel 的具體實施方案焰望。
MVVM 的本質是 數(shù)據(jù)驅動骚亿,把解耦做的更徹底,viewModel不持有view 熊赖。
View 產生事件来屠,使用 ViewModel進行邏輯處理后,通知Model更新數(shù)據(jù),Model把更新的數(shù)據(jù)給ViewModel俱笛,ViewModel自動通知View更新界面捆姜,而不是主動調用View的方法。
MVVM在Android開發(fā)中是如何實現(xiàn)的呢迎膜?接著看~
到這里你會發(fā)現(xiàn)泥技,所謂的架構模式本質上理解很簡單。比如MVP磕仅,甚至你都可以忽略這個名字珊豹,理解成 在更高的層面上 面向接口編程,實現(xiàn)了 依賴倒置 原則榕订,就是這么簡單店茶。
三、MVVM 的實現(xiàn) - Jetpack MVVM
前面提到卸亮,架構模式選擇適合自己項目的即可忽妒。話雖如此,但Google官方推薦的架構模式 是適合大多數(shù)情況兼贸,是非常值得我們學習和實踐的段直。
好了,下面我們就來詳細介紹 Jetpack MVVM 架構溶诞。
3.1 Jetpack MVVM 理解
Jetpack MVVM 是 MVVM 模式在 Android 開發(fā)中的一個具體實現(xiàn)鸯檬,是 Android中 Google 官方提供并推薦的 MVVM實現(xiàn)方式。
不僅通過數(shù)據(jù)驅動完成徹底解耦螺垢,還兼顧了 Android 頁面開發(fā)中其他不可預期的錯誤喧务,例如Lifecycle 能在妥善處理 頁面生命周期 避免view空指針問題,ViewModel使得UI發(fā)生重建時 無需重新向后臺請求數(shù)據(jù)枉圃,節(jié)省了開銷功茴,讓視圖重建時更快展示數(shù)據(jù)。
首先孽亲,請查看下圖坎穿,該圖顯示了所有模塊應如何彼此交互:
各模塊對應MVVM架構:
- View層:Activity/Fragment
- ViewModel層:Jetpack ViewModel + Jetpack LivaData
- Model層:Repository倉庫,包含 本地持久性數(shù)據(jù) 和 服務端數(shù)據(jù)
View層 包含了我們平時寫的Activity/Fragment/布局文件等與界面相關的東西返劲。
ViewModel層 用于持有和UI元素相關的數(shù)據(jù)玲昧,以保證這些數(shù)據(jù)在屏幕旋轉時不會丟失,并且還要提供接口給View層調用以及和倉庫層進行通信篮绿。
倉庫層 要做的主要工作是判斷調用方請求的數(shù)據(jù)應該是從本地數(shù)據(jù)源中獲取還是從網絡數(shù)據(jù)源中獲取孵延,并將獲取到的數(shù)據(jù)返回給調用方。本地數(shù)據(jù)源可以使用數(shù)據(jù)庫亲配、SharedPreferences等持久化技術來實現(xiàn)尘应,而網絡數(shù)據(jù)源則通常使用Retrofit訪問服務器提供的Webservice接口來實現(xiàn)惶凝。
另外,圖中所有的箭頭都是單向的菩收,例如View層指向了ViewModel層梨睁,表示View層會持有ViewModel層的引用,但是反過來ViewModel層卻不能持有View層的引用娜饵。除此之外,引用也不能跨層持有官辈,比如View層不能持有倉庫層的引用箱舞,謹記每一層的組件都只能與它相鄰層的組件進行交互。
這種設計打造了一致且愉快的用戶體驗拳亿。無論用戶上次使用應用是在幾分鐘前還是幾天之前晴股,現(xiàn)在回到應用時都會立即看到應用在本地保留的數(shù)據(jù)。如果此數(shù)據(jù)已過期肺魁,則應用的Repository將開始在后臺更新數(shù)據(jù)电湘。
3.2 實施
我們來舉個完整的例子 - 在頁面中顯示用戶信息列表,來說明 Jetpack MVVM 的具體實施鹅经。
3.2.1 構建界面
首先創(chuàng)建一個列表頁面 UserListActivity寂呛,并且知道頁面所需要的數(shù)據(jù)是,用戶信息列表瘾晃。
那么 用戶信息列表 如何獲取呢贷痪?根據(jù)上面的架構圖,就是ViewModel了蹦误,所以我們創(chuàng)建 UserListViewModel 繼承自 ViewModel劫拢,并且把 用戶信息列表 以 LiveData呈現(xiàn)。
public class UserListViewModel extends ViewModel {
//用戶信息
private MutableLiveData<List<User>> userListLiveData;
//進條度的顯示
private MutableLiveData<Boolean> loadingLiveData;
public UserListViewModel() {
userListLiveData = new MutableLiveData<>();
loadingLiveData = new MutableLiveData<>();
}
public LiveData<List<User>> getUserListLiveData() {
return userListLiveData;
}
public LiveData<Boolean> getLoadingLiveData() {
return loadingLiveData;
}
...
}
復制代碼
LiveData 是一種可觀察的數(shù)據(jù)存儲器强胰。應用中的其他組件可以使用此存儲器監(jiān)控對象的更改舱沧,而無需在它們之間創(chuàng)建明確且嚴格的依賴路徑。LiveData 組件還遵循應用組件(如 Activity偶洋、Fragment 和 Service)的生命周期狀態(tài)熟吏,并包括清理邏輯以防止對象泄漏和過多的內存消耗。
將 UserListViewModel 中的字段類型更改為 MutableLiveData<List>∥姓妫現(xiàn)在分俯,更新數(shù)據(jù)時,系統(tǒng)會通知 UserListActivity哆料。此外缸剪,由于此 LiveData 字段具有生命周期感知能力,因此當不再需要引用時东亦,會自動清理它們杏节。
另外背捌,注意到暴露的獲取LiveData的方法 返回的是LiveData類型,即不可變的窥浪,而不是MutableLiveData物臂,好處是避免數(shù)據(jù)在外部被更改。(參見LivaData篇文章)
現(xiàn)在嫉鲸,我們修改 UserListActivity 以觀察數(shù)據(jù)并更新界面:
//UserListActivity.java
...
//觀察ViewModel的數(shù)據(jù)撑蒜,且此數(shù)據(jù) 是 View 直接需要的,不需要再做邏輯處理
private void observeLivaData() {
mUserListViewModel.getUserListLiveData().observe(this, new Observer<List<User>>() {
@Override
public void onChanged(List<User> users) {
if (users == null) {
Toast.makeText(UserListActivity.this, "獲取user失斝座菠!", Toast.LENGTH_SHORT).show();
return;
}
//刷新列表
mUserAdapter.setNewInstance(users);
}
});
mUserListViewModel.getLoadingLiveData().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(Boolean aBoolean) {
//顯示/隱藏加載進度條
mProgressBar.setVisibility(aBoolean? View.VISIBLE:View.GONE);
}
});
}
復制代碼
每次更新用戶列表信息數(shù)據(jù)時,系統(tǒng)都會調用 onChanged() 回調并刷新界面藤树,而不需要 ViewModel主動調用View層方法刷新浴滴,這就是 數(shù)據(jù)驅動 了 —— 數(shù)據(jù)的更改 驅動 View 自動刷新。
因為LiveData具有生命周期感知能力岁钓,這意味著升略,除非 Activity 處于活躍狀態(tài),否則它不會調用 onChanged() 回調屡限。當調用 Activity 的 onDestroy() 方法時品嚣,LiveData 還會自動移除觀察者。
另外囚霸,我們也沒有添加任何邏輯來處理配置更改(例如腰根,用戶旋轉設備的屏幕)。UserListViewModel 會在配置更改后自動恢復拓型,所以一旦創(chuàng)建新的 Activity额嘿,它就會接收相同的 ViewModel 實例,并且會立即使用當前的數(shù)據(jù)調用回調劣挫。鑒于 ViewModel 對象應該比它們更新的相應 View 對象存在的時間更長册养,因此 ViewModel 實現(xiàn)中不得包含對 View 對象的直接引用,包括Context压固。
3.2.2 獲取數(shù)據(jù)
現(xiàn)在球拦,我們已使用 LiveData 將 UserListViewModel 連接到UserListActivity,那么如何獲取用戶個人信息列表數(shù)據(jù)呢帐我?
實現(xiàn) ViewModel 的第一個想法可能是 使用Retrofit/Okhttp調用接口 來獲取數(shù)據(jù)坎炼,然后將該數(shù)據(jù)設置給 LiveData 對象。這種設計行得通拦键,但如果采用這種設計谣光,隨著應用的擴大,應用會變得越來越難以維護芬为。這樣會使 UserListViewModel 類承擔太多的責任萄金,這就違背了單一職責原則蟀悦。
ViewModel 會將數(shù)據(jù)獲取過程委派給一個新的模塊,即Repository氧敢。
Repository模塊會處理數(shù)據(jù)操作日戈。它們會提供一個干凈的 API,以便應用內其余部分也可以輕松獲取該數(shù)據(jù)孙乖。數(shù)據(jù)更新時浙炼,它們知道從何處獲取數(shù)據(jù)以及進行哪些 API 調用。您可以將Repository視為不同數(shù)據(jù)源(如持久性模型唯袄、網絡服務和緩存)之間的媒介鼓拧。
public class UserRepository {
private static UserRepository mUserRepository;
public static UserRepository getUserRepository(){
if (mUserRepository == null) {
mUserRepository = new UserRepository();
}
return mUserRepository;
}
//(假裝)從服務端獲取
public void getUsersFromServer(Callback<List<User>> callback){
new AsyncTask<Void, Void, List<User>>() {
@Override
protected void onPostExecute(List<User> users) {
callback.onSuccess(users);
//存本地數(shù)據(jù)庫
saveUsersToLocal(users);
}
@Override
protected List<User> doInBackground(Void... voids) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//假裝從服務端獲取的
List<User> users = new ArrayList<>();
for (int i = 0; i < 20; i++) {
User user = new User("user"+i, i);
users.add(user);
}
return users;
}
}.execute();
}
復制代碼
雖然Repository模塊看起來不必要,但它起著一項重要的作用:它會從應用的其余部分中提取數(shù)據(jù)源≡铰瑁現(xiàn)在,UserListViewModel 是不知道數(shù)據(jù)來源的钮糖,因此我們可以為ViewModel提供從幾個不同的數(shù)據(jù)源獲取數(shù)據(jù)梅掠。
3.2.3 連接 ViewModel 與存儲區(qū)
我們在UserListViewModel 提供一個方法,用戶Activity獲取用戶信息店归。此方法就是調用Repository來執(zhí)行阎抒,并且吧數(shù)據(jù)設置到LiveData。
public class UserListViewModel extends ViewModel {
//用戶信息
private MutableLiveData<List<User>> userListLiveData;
//進條度的顯示
private MutableLiveData<Boolean> loadingLiveData;
public UserListViewModel() {
userListLiveData = new MutableLiveData<>();
loadingLiveData = new MutableLiveData<>();
}
/**
* 獲取用戶列表信息
* 假裝網絡請求 2s后 返回用戶信息
*/
public void getUserInfo() {
loadingLiveData.setValue(true);
UserRepository.getUserRepository().getUsersFromServer(new Callback<List<User>>() {
@Override
public void onSuccess(List<User> users) {
loadingLiveData.setValue(false);
userListLiveData.setValue(users);
}
@Override
public void onFailed(String msg) {
loadingLiveData.setValue(false);
userListLiveData.setValue(null);
}
});
}
//返回LiveData類型
public LiveData<List<User>> getUserListLiveData() {
return userListLiveData;
}
public LiveData<Boolean> getLoadingLiveData() {
return loadingLiveData;
}
}
復制代碼
在Activity中消痛,就要獲取UserListViewModel實例且叁,獲取用戶信息:
//UserListActivity.java
public class UserListActivity extends AppCompatActivity {
private UserListViewModel mUserListViewModel;
private ProgressBar mProgressBar;
private RecyclerView mRvUserList;
private UserAdapter mUserAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
initView();
initViewModel();
getData();
observeLivaData();
}
private void initView() {...}
private void initViewModel() {
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
mUserListViewModel = viewModelProvider.get(UserListViewModel.class);
}
/**
* 獲取數(shù)據(jù),調用ViewModel的方法獲取
*/
private void getData() {
mUserListViewModel.getUserInfo();
}
private void observeLivaData() {...}
復制代碼
3.2.4 緩存數(shù)據(jù)
現(xiàn)在UserRepository 有個問題是秩伞,它從后端獲取數(shù)據(jù)后逞带,不會將緩存該數(shù)據(jù)。因此纱新,如果用戶在離開頁面后再返回展氓,則應用必須重新獲取數(shù)據(jù),即使數(shù)據(jù)未發(fā)生更改也是如此脸爱。這就浪費了寶貴的網絡資源遇汞,迫使用戶等待新的查詢完成。 所以簿废,我們向 UserRepository 添加了一個新的數(shù)據(jù)源空入,本地緩存。緩存實現(xiàn) 可以是 數(shù)據(jù)庫族檬、SharedPreferences等持久化技術歪赢。(具體實現(xiàn)就不再寫了)
//UserRepository.java
//從本地數(shù)據(jù)庫獲取
public void getUsersFromLocal(){
// TODO: 2021/1/24 從本地數(shù)據(jù)庫獲取
}
//存入本地數(shù)據(jù)庫 (從服務端獲取數(shù)據(jù)后可以調用)
private void saveUsersToLocal(List<User> users){
// TODO: 2021/1/24 存入本地數(shù)據(jù)庫
}
復制代碼
到這里,Jetpack MVVM 就介紹完了导梆。
實際上只要前面介紹的 Lifecycle轨淌、LivaData迂烁、ViewModel 熟練掌握的話,這里是十分好理解的递鹉。
3.3 注意點
在應用的各個模塊之間設定明確定義的職責界限盟步。
ViewModel 不能持有 View層引用,包括Context也不能持有躏结。
將一個數(shù)據(jù)源指定為單一可信來源却盘。 每當需要訪問數(shù)據(jù)時,都應一律源于此單一可信來源媳拴。 例如 UserRepository會將網絡服務響應保存在數(shù)據(jù)庫中黄橘。這樣一來,對數(shù)據(jù)庫的更改將觸發(fā)對活躍 LiveData 對象的回調屈溉。數(shù)據(jù)庫會充當單一可信來源塞关。
保留盡可能多的相關數(shù)據(jù)和最新數(shù)據(jù)。 這樣子巾,即使用戶的設備處于離線模式帆赢,他們也可以使用您應用的功能。請注意线梗,并非所有用戶都能享受到穩(wěn)定的高速連接椰于。
顯示頁面狀態(tài)。 例如例子中的加載進度條仪搔,就是觀察 ViewModel中的MutableLiveData loadingLiveData 進行操作的瘾婿。
3.4 MVP改造MVVM
了解了Jetpack MVVM的實現(xiàn),再來改造 MVP 是很簡單的了烤咧。
步驟如下:
- 去除Presener 對View偏陪、context的引用。
- Presener 替換成ViewModel的實現(xiàn)髓削,獲取的數(shù)據(jù)以 LivaData呈現(xiàn)竹挡。
- 刪除定義的IView等接口,Activity/Fragment中 獲取ViewModel實例立膛,調用其方法獲取數(shù)據(jù)揪罕。
- Activity/Fragment 觀察需要的 LivaData 然后刷新UI。
這樣就已經成為了MVVM宝泵。當然也要檢查下 原MVP的 Model層的實現(xiàn)好啰,是否滿足上面的要求。
四儿奶、總結
本篇介紹了 架構模式的含義框往,回顧和比較了Android中的架構模式MVC、MVP闯捎、MVVM椰弊,最好在 Jetpack架構組件 基礎上 介紹了 MVVM 的詳細實現(xiàn)方法许溅、注意點,以及MVP的改造秉版。
整篇下來贤重,基本很簡單容易理解的。 例子是很簡單的清焕,所以在實際開發(fā)中 需要深入理解 MVVM 數(shù)據(jù)驅動的本質并蝗,和MVP的區(qū)別。
有人可能會有疑惑:怎么完全沒有提 DataBinding秸妥、雙向綁定滚停?
實際上,這也是我之前的疑惑粥惧。 沒有提 是因為:
- 我不想讓讀者 一提到 MVVM 就和DataBinding聯(lián)系起來
- 我想讓讀者 抓住 MVVM 數(shù)據(jù)驅動 的本質键畴。
- 而DataBinding提供的雙向綁定,是用來完善Jetpack MVVM 的工具突雪,其本身在業(yè)界又非常具有爭議性镰吵。
- 掌握本篇內容,已經是Google推薦的開發(fā)架構挂签,就已經實現(xiàn) MVVM 模式。在Google官方的 應用架構指南 中 也同樣絲毫沒有提到 DataBinding盼产。
所以饵婆,下一篇,將繼續(xù)介紹 Jetpack AAC 的組件:數(shù)據(jù)綁定組件 DataBinding戏售、數(shù)據(jù)庫組件 Room侨核,作為 Jetpack MVVM 的完善補充點。 并且 也將是 Jetpack AAC 完整解析 系列的最后一篇灌灾。 敬請期待搓译!
五、分享
分享一份《Jetpack架構組件從入門到精通》的pdf學習筆記給大家锋喜,內容涵括了Jetpack幾乎所有你能想到的知識點些己,而每一個知識點都有詳細的源碼解析,以及實戰(zhàn)講解嘿般!