簡評(píng):MVVM 是谷歌提出的一種 Android 架構(gòu)模式裹匙,結(jié)合了 Data Binding 和一些生命周期組件 LiveData 和 ViewModel 等。詳情可以查看谷歌官方樣例庫迫淹。
View 和 ViewModels
理論情況下旬牲,ViewModel 不需要知道任何關(guān)于 Android 的東西。這提供了可測(cè)試性,防止內(nèi)存泄漏和模塊化的好處俐填。一條基本規(guī)制是確保在你的 ViewModels 類中沒有任何 android.* 的類導(dǎo)入(android.arch.* 例外)。這點(diǎn)和 Presenter 一樣翔忽。
??別讓 ViewModels(和 Presenters)知道任何關(guān)于 Android 框架的類英融。
條件語句,循環(huán)和一般決策應(yīng)該在 app 的 ViewModels 或其他層次中完成歇式,而不是在 Activity 或 Fragment 中驶悟。View 通常不是單元測(cè)試的(除非你使用?Robolectric),所以越少代碼越好材失。View 僅僅應(yīng)該知道如何展示數(shù)據(jù)以及發(fā)送用戶的事件給 ViewModel(或 Presenter)痕鳍。這就叫被動(dòng)視圖模式。
??在 Activities 和 Fragments 中保持最小化的邏輯
在 ViewModels 中引用 View
ViewModels?和 Activities 或 Fragments 相比有著不同的范圍。當(dāng)一個(gè) ViewModel 存活并運(yùn)行時(shí)笼呆,一個(gè) Activity 可以處于它生命周期的任何一個(gè)狀態(tài)熊响。在 ViewModel 不知道的情況下,Activities 和 Fragments 可以被再次摧毀和創(chuàng)建诗赌。
ViewModels 在配置改變時(shí)仍然存在
將 View 的引用(Activity 或 Fragment)傳遞到 ViewModel 是一個(gè)嚴(yán)重的風(fēng)險(xiǎn)汗茄。假設(shè) ViewModel 中請(qǐng)求了網(wǎng)絡(luò)數(shù)據(jù),數(shù)據(jù)在之后的一段時(shí)間返回铭若。此時(shí)洪碳,View 的引用有可能被回收或者舊的 Activity 不再可見,這樣就產(chǎn)生了內(nèi)存泄漏奥喻,甚至崩潰偶宫。
??避免在 ViewModels 中引用 Views
在 ViewModels 和 Views 中溝通推薦的方式是觀察者模式,利用 LiveData 或者其他庫提供的可觀察的方式环鲤。
觀察者模式
通過觀察者模式,在 Android 展示層中讓 View(Activity 或 Fragment)觀察(訂閱改變) ViewModel 顯得非常方便憎兽。因?yàn)?ViewModel 不需要知道 Android 內(nèi)容冷离,它也不知道 Android 是如何頻繁地殺死 View 的。好處在于:
ViewModels 在配置改變時(shí)依然存在纯命,因此當(dāng)旋轉(zhuǎn)屏幕時(shí)不需要再次查詢內(nèi)部數(shù)據(jù)(數(shù)據(jù)庫或網(wǎng)絡(luò))西剥。
當(dāng)一個(gè)長時(shí)間的操作結(jié)束時(shí),在 ViewModel 中可觀察的部分更新了亿汞。無論數(shù)據(jù)是否被觀察了瞭空,當(dāng)嘗試更新不存在的 View 時(shí)沒有空指針異常的發(fā)生。
ViewModels 不引用 View疗我,所以減少了內(nèi)存泄漏的風(fēng)險(xiǎn)咆畏。
private void subscribeToModel() {
? // Observe product data
? viewModel.getObservableProduct().observe(this, new Observer() {
? ? ? @Override
? ? ? public void onChanged(@Nullable Product product) {
? ? ? ? mTitle.setText(product.title);
? ? ? }
? });
}
??在 UI 中注冊(cè)數(shù)據(jù)推送,讓 UI 觀察它的改變吴裤。
臃腫的 ViewModels
如果你的 ViewModel 中放了太多代碼或者太多的職責(zé)旧找,可以考慮:
將一些邏輯移動(dòng)到 presenter,它和 ViewModel 有相同的范圍麦牺。它可以和你 app 中的其他部分溝通并更新 ViewModel 中 LiveData 的持有者钮蛛。
添加一個(gè)領(lǐng)域?qū)硬⒉扇?a target="_blank" rel="nofollow">簡潔的架構(gòu),這樣有利于測(cè)試和維護(hù)剖膳,也同樣促進(jìn)了快速脫離主線程魏颓。在?架構(gòu)藍(lán)圖中有一個(gè)簡潔架構(gòu)的樣例。
??必要時(shí)分散職責(zé)吱晒,增加領(lǐng)域?qū)印?/b>
使用數(shù)據(jù)倉庫
在 App 架構(gòu)指南中甸饱,可以看到大多數(shù)應(yīng)用都有多種數(shù)據(jù)來源,比如:
遠(yuǎn)程:網(wǎng)絡(luò)或云
本地:數(shù)據(jù)庫或文件
內(nèi)存緩存
在你的應(yīng)用中擁有數(shù)據(jù)層是一個(gè)好主意枕荞,展示層完全注意不到柜候。保留緩存搞动、同步數(shù)據(jù)庫和網(wǎng)絡(luò)的算法都是無關(guān)緊要的。擁有一個(gè)完全隔離的倉庫類作為一個(gè)單一的入口來處理這些復(fù)雜的事情是非常推薦的渣刷。
如果你有多種并且不同的數(shù)據(jù)模型鹦肿,考慮添加多種倉庫。
??添加一個(gè)數(shù)據(jù)倉庫作為訪問數(shù)據(jù)的單一入口辅柴。
處理數(shù)據(jù)狀態(tài)
考慮這個(gè)場景:你正在觀察 ViewModel 中暴露的 LiveData箩溃,它包含一個(gè)展示項(xiàng)目的列表。那么在數(shù)據(jù)加載碌嘀,網(wǎng)絡(luò)錯(cuò)誤或者空列表時(shí)涣旨,視圖該如何呈現(xiàn)這些變化呢?
你可以在 ViewModel 中暴露一個(gè) LiveData股冗。例如霹陡,MyDataState 應(yīng)該包含那些數(shù)據(jù)是否正在加載,或者加載成功或加載失敗的信息止状。
你可以把數(shù)據(jù)包裹在一個(gè)保存了狀態(tài)或其他元數(shù)據(jù)(例如一條錯(cuò)誤消息)的類中烹棉。看看我們樣例中的?Resource?類怯疤。
??使用一些包裹類或另一個(gè) LiveData 來暴露你數(shù)據(jù)的信息浆洗。
保存 Activity 狀態(tài)
Activity 狀態(tài)是一些當(dāng) Activity 消失時(shí)(意味著被回收或進(jìn)程被殺死)你需要重新恢復(fù)屏幕的信息。旋轉(zhuǎn)屏幕是一個(gè)最顯著的案例集峦,還好我們有 ViewModel伏社。狀態(tài)保存在 ViewModel 中是安全的。
然而在某些場景中塔淤,當(dāng) ViewModel 也消失時(shí)摘昌,你可能也需要恢復(fù)狀態(tài)。比如當(dāng)操作系統(tǒng)的資源緊缺時(shí)凯沪,可能會(huì)殺死你的進(jìn)程第焰。
為了高效地保存和恢復(fù) UI 狀態(tài),需要結(jié)合持續(xù)性妨马,onSaveInstanceState() 以及 ViewModels挺举。
看看例子:ViewModels:持續(xù),onSaveInstanceState()烘跺,恢復(fù) UI 狀態(tài)和裝載器
事件
一個(gè)事件指發(fā)送一次的動(dòng)作湘纵。ViewModels 暴露了數(shù)據(jù),但什么是事件呢滤淳?例如梧喷,導(dǎo)航事件或者展示 Snackbar 消息都是應(yīng)該只執(zhí)行一次的動(dòng)作。
事件的概念并不能很好的展示 LiveData 是如何存儲(chǔ)和恢復(fù)數(shù)據(jù)的。來看一下下面的 ViewModel:
LiveData snackbarMessage = new MutableLiveData<>();
一個(gè) Activity 開始觀察這個(gè)數(shù)據(jù)铺敌,并且 ViewModel 完成了一個(gè)操作之后它需要更新這條消息:
snackbarMessage.setValue("Item saved!");
Activity 收到這條消息汇歹,并展示在 Snackbar 中。這顯然沒毛病偿凭。
然而产弹,如果用戶旋轉(zhuǎn)手機(jī),創(chuàng)建了新的 Activity 并開始觀察弯囊。當(dāng) LiveData 觀察發(fā)生后痰哨,Activity 立即收到了舊的值,這時(shí)消息再次展示了匾嘱!
我們擴(kuò)展了 LiveData斤斧,并創(chuàng)建了一個(gè)類叫?SingleLiveEvent,作為剛剛問題的解決方案霎烙。它僅僅發(fā)送訂閱之后出現(xiàn)的更新撬讽。注意它只支持一個(gè)觀察者。
??為像導(dǎo)航或 Snackbar 消息等事件使用可觀察的行為如?SingleLiveEvent悬垃。
泄露 ViewModels
反應(yīng)式范例在 Android 中工作得很好锐秦,因?yàn)樗试S在 UI 和應(yīng)用的其他層次建立方便的連接。LiveData 是這個(gè)結(jié)構(gòu)中的關(guān)鍵組件盗忱,因此通常情況下你的 Activities 和 Fragments 都會(huì)觀察一個(gè) LiveData 實(shí)例。
ViewModels 和其他組件是如何溝通的取決于你羊赵,但要注意泄露和邊界情況趟佃。下圖中展示層使用了觀察者模式,數(shù)據(jù)層使用了回調(diào):
如果用戶退出了應(yīng)用昧捷,View 將會(huì)消失闲昭,因此 ViewModel 不再被觀察。如果倉庫是一個(gè)單例靡挥,或者應(yīng)用范圍的序矩,那么倉庫將不會(huì)回收直到進(jìn)程被殺死。這只會(huì)在操作系統(tǒng)需要資源或者用戶手動(dòng)殺死應(yīng)用時(shí)才會(huì)發(fā)生跋破。如果倉庫保留了 ViewModel 中回調(diào)的引用簸淀,那么 ViewModel 就會(huì)暫時(shí)泄露。
如果 ViewModel 存活或者被分配的操作很快就完成了毒返,那么這個(gè)泄露沒什么租幕。然而,不是所有的時(shí)候都這樣拧簸。理想情況下劲绪,ViewModels 應(yīng)該在沒有任何 Views 觀察它們時(shí)回收:
你可以采取以下選項(xiàng)來達(dá)到這個(gè)目的:
使用?ViewModel.onCleared()?你可以告訴倉庫扔掉 ViewModel 的回調(diào)。
在倉庫中你可以使用弱引用或者?EventBus(這兩者都容易濫用甚至有害)
??考慮邊界情況,泄露以及長時(shí)間的操作如何影響你架構(gòu)中的實(shí)例贾富。
??不要把保存清除狀態(tài)或者相關(guān)的關(guān)鍵邏輯放在 ViewModel 中歉眷。任何你從 ViewModel 中的調(diào)用都可能是最后一次。
在倉庫中的 LiveData
為了避免 ViewModels 的泄露和回調(diào)地獄颤枪,倉庫可以這樣觀察:
當(dāng) ViewModel 被清除時(shí)或者當(dāng) View 的生命周期結(jié)束時(shí)汗捡,訂閱也被清除了:
如果用這種方式的話有個(gè) catch:如果你不訪問 LifecycleOwner,那么你怎么從倉庫訂閱 ViewModel 呢汇鞭?使用?Transformations?可以很方便地解決這個(gè)問題凉唐。Transformations.switchMap 讓你可以創(chuàng)建一個(gè)新的 LiveData,可以對(duì)其他 LiveData 實(shí)例做出反應(yīng)霍骄。它也同樣允許你通過鏈來攜帶觀察者的生命周期信息:
LiveData repo = Transformations.switchMap(repoIdLiveData, repoId -> {
? ? ? ? if (repoId.isEmpty()) {
? ? ? ? ? ? return AbsentLiveData.create();
? ? ? ? }
? ? ? ? return repository.loadRepo(repoId);
? ? }
);
在這個(gè)例子中台囱,當(dāng)觸發(fā)更新時(shí),函數(shù)就會(huì)被調(diào)用并將結(jié)果向下分發(fā)读整。一個(gè) Activity 可以觀察?repo?并且隨著repository.loadRepo(id)?的調(diào)用相同的 LifecycleOwner 會(huì)被使用簿训。
? 無論何時(shí),當(dāng)你考慮在?ViewModel?中使用?生命周期對(duì)象時(shí)米间,Transformation?都可能是個(gè)解決方案强品。
繼承 LiveData
LiveData 最普遍的用例就是在 ViewModels 中使用MutableLiveData?并且將它們作為?LiveData?暴露出去,使得它們?cè)谄渌^察者中不可改變屈糊。
如果你需要更多功能的榛,繼承 LiveData 可以讓你知道何時(shí)觀察者正在活動(dòng)。當(dāng)你想要開始監(jiān)聽定位或者傳感器服務(wù)時(shí)這將會(huì)很有用逻锐。例如:
public class MyLiveData extends LiveData {
? ? public MyLiveData(Context context) {
? ? ? ? // Initialize service
? ? }
? ? @Override
? ? protected void onActive() {
? ? ? ? // Start listening
? ? }
? ? @Override
? ? protected void onInactive() {
? ? ? ? // Stop listening
? ? }
}
何時(shí)不繼承 LiveData
你也可以使用?onActive()?開始一些加載數(shù)據(jù)的服務(wù)夫晌,但除非你有一個(gè)很好的理由,你沒有必要等待 LiveData 被觀察昧诱。一些普遍的模式:
在 ViewModel 中添加一個(gè)?start()?方法晓淀,并盡快調(diào)用它。[查看藍(lán)圖例子]
設(shè)置一個(gè)啟動(dòng)負(fù)載的屬性[查看?GithubBrowserExample]
??通常你不用繼承 LiveData盏档。讓你的 Activity 或者 Fragment 來告訴 ViewModel 什么時(shí)候開始加載數(shù)據(jù)凶掰。