【目錄】
1. Architecture Components 之 Guide to App Architecture
2. Architecture Components 之 Adding Components to your Project
3. Architecture Components 之 Handling Lifecycles
4. Architecture Components 之 LiveData
5. Architecture Components 之 ViewModel
6. Architecture Components 之 Room Persistence Library
示例代碼鏈接
</br>
應(yīng)用程序架構(gòu)指南
本指南使用于具有構(gòu)建應(yīng)用程序基礎(chǔ)并且想了解構(gòu)建強(qiáng)大、優(yōu)質(zhì)的應(yīng)用程序的最佳實(shí)踐和推薦架構(gòu)的開(kāi)發(fā)人員垫桂。
注:本指南假定讀者熟悉 Android Framework。如果你是一個(gè)應(yīng)用程序開(kāi)發(fā)的新手,請(qǐng)參閱入門指南系列培訓(xùn),其中包含了本指南先決條件的相關(guān)主題拄衰。
應(yīng)用開(kāi)發(fā)者面臨的常見(jiàn)問(wèn)題
在大多數(shù)情況下,桌面應(yīng)用程序在啟動(dòng)器快捷方式中有一個(gè)單一的入口并且作為單獨(dú)的獨(dú)立進(jìn)程運(yùn)行,與桌面應(yīng)用程序不同苛让,Android 應(yīng)用具有更復(fù)雜的結(jié)構(gòu)。一個(gè)典型的 Android 應(yīng)用是由多個(gè)應(yīng)用程序組件構(gòu)成的湿诊,包括 activity狱杰,fragment,service厅须,content provider 和 broadcast receiver仿畸。
這些應(yīng)用程序組件中的大部分聲明在由 Android OS 使用的應(yīng)用程序清單中,用來(lái)決定如何將應(yīng)用融入到用戶設(shè)備的整體體驗(yàn)中朗和。盡管如前所述错沽,傳統(tǒng)的桌面應(yīng)用程序作為獨(dú)立進(jìn)程運(yùn)行,但是正確的編寫(xiě) Android 應(yīng)用程序需要更加靈活眶拉,因?yàn)橛脩魰?huì)同過(guò)設(shè)備上不同的應(yīng)用程序組織成自己的方式不斷切換流程和任務(wù)千埃。
例如,考慮下在你喜歡的社交網(wǎng)絡(luò)應(yīng)用中分享照片時(shí)會(huì)發(fā)生什么镀层。該應(yīng)用會(huì)觸發(fā)一個(gè)啟動(dòng)相機(jī)的 intent镰禾,從該 intent 中 Android OS 會(huì)啟動(dòng)一個(gè)相機(jī)應(yīng)用來(lái)處理這個(gè)請(qǐng)求皿曲。在此刻唱逢,用戶離開(kāi)社交網(wǎng)絡(luò)應(yīng)用但是用戶的體驗(yàn)是無(wú)縫的。相機(jī)應(yīng)用轉(zhuǎn)而可能會(huì)觸發(fā)其它的 intent屋休,例如啟動(dòng)文件選擇器坞古,這可能會(huì)啟動(dòng)另一個(gè)應(yīng)用。最終用戶回到社交網(wǎng)絡(luò)應(yīng)用并且分享照片劫樟。此外痪枫,在這個(gè)過(guò)程中的任何時(shí)刻用戶都有可能會(huì)被一個(gè)電話打斷织堂,并且在結(jié)束通話后再回來(lái)繼續(xù)分享照片。
在 Android 中奶陈,這種應(yīng)用切換行為很常見(jiàn)易阳,所以你的應(yīng)用程序必須正確處理這些流程。記住吃粒,移動(dòng)設(shè)備的資源是有限的潦俺,所以在任何時(shí)候,操作系統(tǒng)都可能會(huì)殺死一些應(yīng)用為新的應(yīng)用騰出空間徐勃。
其中的重點(diǎn)是應(yīng)用程序組件可能會(huì)被單獨(dú)和無(wú)序的啟動(dòng)事示,并且可能會(huì)被用戶或系統(tǒng)在任何時(shí)候銷毀。因?yàn)閼?yīng)用程序組件是短暫的僻肖,并且其聲明周期(什么時(shí)候被創(chuàng)建和銷毀)不受你控制肖爵,所以不應(yīng)該在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用數(shù)據(jù)或狀態(tài),同時(shí)應(yīng)用程序組件不應(yīng)該相互依賴臀脏。
常見(jiàn)的架構(gòu)原則
如果不能在應(yīng)用程序組件中存儲(chǔ)應(yīng)用數(shù)據(jù)和狀態(tài)劝堪,那么應(yīng)該如何構(gòu)建應(yīng)用?
最重要的是在應(yīng)用中要專注于關(guān)注點(diǎn)分離揉稚。一個(gè)常見(jiàn)的錯(cuò)誤是在 Activity 或 Fragment 中編寫(xiě)所有的代碼幅聘。任何不是處理 UI 或 操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中。保持它們盡可能的精簡(jiǎn)可以避免許多與生命周期有關(guān)的問(wèn)題窃植。不要忘記你不擁有這些類帝蒿,它們只是體現(xiàn)了 OS 和 應(yīng)用之間協(xié)議的粘合類。Android OS 可能會(huì)因?yàn)橛脩艚换セ蚱渌蛩兀ㄈ绲蛢?nèi)存)的原因在任何時(shí)候銷毀它們巷怜。最好盡量減少對(duì)它們的依賴以提供一個(gè)穩(wěn)固的用戶體驗(yàn)葛超。
第二個(gè)重要的原則是應(yīng)該用 Model 驅(qū)動(dòng) UI,最好是持久化的 Model延塑。持久化是最佳的原因有兩個(gè):一是如果 OS 銷毀應(yīng)用釋放資源绣张,用戶不用擔(dān)心丟失數(shù)據(jù);二是即使網(wǎng)絡(luò)連接不可靠或者是斷開(kāi)的关带,應(yīng)用仍將繼續(xù)運(yùn)行侥涵。Model 是負(fù)責(zé)處理應(yīng)用數(shù)據(jù)的組件。Modle 獨(dú)立于應(yīng)用中的 View 和應(yīng)用程序組件宋雏,因此 Model 和這些組件的生命周期問(wèn)題隔離開(kāi)了芜飘。保持 UI 代碼精簡(jiǎn)并且摒除應(yīng)用的邏輯使其更易于管理∧プ埽基于 Model 類構(gòu)建的應(yīng)用程序其管理數(shù)據(jù)的職責(zé)明確嗦明,使應(yīng)用程序可測(cè)試并且穩(wěn)定。
推薦的應(yīng)用程序架構(gòu)
在本節(jié)中蚪燕,我們將通過(guò)一個(gè)用例來(lái)演示如何使用 Architecture Components 構(gòu)建應(yīng)用程序娶牌。
注:不可能有一種應(yīng)用程序的編寫(xiě)方式對(duì)于每種情況都是最好的奔浅。話雖如此,這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的良好起點(diǎn)诗良。如果你已經(jīng)有一種很好的應(yīng)用程序編寫(xiě)方式則不需要改變汹桦。
假設(shè)我們正在構(gòu)建一個(gè)顯示用戶個(gè)人信息的 UI。用戶的個(gè)人信息將使用 REST API 從我們自己的私有后端獲取鉴裹。
構(gòu)建用戶界面
UI 包含一個(gè) fragment 文件 UserProfileFragment.java 和其布局文件 user_profile_layout.xml营勤。
為了驅(qū)動(dòng) UI,數(shù)據(jù)模型需要持有兩個(gè)數(shù)據(jù)元素壹罚。
用戶 ID :用戶的標(biāo)識(shí)符葛作。最好使用 fragment 的參數(shù)將用戶 ID 傳到 fragment 中。如果 Android OS 銷毀進(jìn)程猖凛,該 ID 將會(huì)被保存赂蠢,以便下次應(yīng)用重啟時(shí)該 ID 可用。
用戶對(duì)象:保存用戶數(shù)據(jù)的普通 Java 對(duì)象(POJO)辨泳。
我們將會(huì)基于 ViewModel 來(lái)創(chuàng)建一個(gè) UserProfileViewModel 來(lái)保存這些信息虱岂。
<p><strong><a >ViewModel</a></strong> 為指定的 UI 組件(如:fragment 或 activity)提供數(shù)據(jù),并且負(fù)責(zé)與數(shù)據(jù)處理的業(yè)務(wù)部分的交互菠红,例如:調(diào)用其它組件獲取數(shù)據(jù)或轉(zhuǎn)發(fā)用戶的操作第岖。ViewModel 對(duì)于 View 并不了解并且不受配置改變(如:由于旋轉(zhuǎn)導(dǎo)致 activity 的重新創(chuàng)建)的影響</p>
現(xiàn)在有 3 個(gè)文件
user_profile.xml:屏幕上的 UI 定義。
UserProfileViewModel.java:為 UI 準(zhǔn)備數(shù)據(jù)的類试溯。
UserProfileFragment.java:用于在 ViewModel 中顯示數(shù)據(jù)并對(duì)用戶交互做出反應(yīng)的 UI 控制器蔑滓。
下面是我們的初始實(shí)現(xiàn)(簡(jiǎn)單起見(jiàn)省略布局文件):
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
public class UserProfileFragment extends LifecycleFragment {
private static final String UID_KEY = "uid";
private UserProfileViewModel viewModel;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String userId = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile, container, false);
}
}
注:上面的例子繼承了 LifecycleFragment 而不是 Fragment 類。在 Architecture Components 中的生命周期 API 穩(wěn)定后遇绞, Android 支持包中的 Fragment 類將會(huì)實(shí)現(xiàn) LifecycleOwner 接口键袱。
現(xiàn)在我們有了 3 個(gè)代碼模塊,怎樣連接它們摹闽?最后蹄咖,當(dāng) ViewModel 的用戶字段被設(shè)置時(shí),需要一種方式來(lái)通知 UI付鹿。這正是 LiveData 的用武之地澜汤。
<p><strong><a >LiveData</a></strong> 是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察 <strong><a >LiveData</a></strong> 進(jìn)行改變舵匾,而不會(huì)在組件之間創(chuàng)建顯示的俊抵,固定的依賴。另外纽匙,LiveData 還遵守應(yīng)用程序組件(如:activity务蝠,fragment拍谐,service)的生命周期狀態(tài)烛缔,并且防止對(duì)象泄漏使應(yīng)用不會(huì)消耗更多的內(nèi)存馏段。</p>
注:如果你已經(jīng)再使用像 RxJava 或 Agera 的庫(kù),你可以繼續(xù)使用它們而不用換成 LiveData践瓷。但是當(dāng)使用它們或其它的方式時(shí)院喜,請(qǐng)確保正確處理生命周期,如:當(dāng)相關(guān) LifecycleOwner 停止時(shí)暫停數(shù)據(jù)流或在 LifecycleOwner 被銷毀時(shí)銷毀數(shù)據(jù)流晕翠∨缫ǎ可以添加 android.arch.lifecycle:reactivestreams 工具,和其它的響應(yīng)流庫(kù)(如:RxJava2)一起使用 LiveData淋肾。
將 UserProfileViewModel 中的 User 字段替換為 LiveData <User>硫麻,以便在更新數(shù)據(jù)時(shí)可以通知 fragment。 LiveData 的好處在于它是生命周期感知的樊卓,并且可以在不再被需要的時(shí)候自動(dòng)清除引用拿愧。
public class UserProfileViewModel extends ViewModel {
...
// private User user;
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
修改 UserprofileFragment 來(lái)觀察數(shù)據(jù)并更新 UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// 更新 UI
});
}
每次更新用戶數(shù)據(jù)時(shí)碌尔,將會(huì)調(diào)用 onChange 回調(diào)并且更新 UI浇辜。
如果你熟悉其它庫(kù)的可觀察回調(diào)的使用,可能已經(jīng)意識(shí)到我們沒(méi)有重寫(xiě) fragment 的 onStop() 方法來(lái)停止觀察數(shù)據(jù)唾戚。這對(duì)于 LiveData 來(lái)說(shuō)是不必要的柳洋,因?yàn)?LiveData 是證明周期感知的,這意味著除非 fragment 處于活動(dòng)狀態(tài)(收到了 onStart() 但還沒(méi)有收到 onStop())叹坦,否則它不會(huì)調(diào)用回調(diào)熊镣。當(dāng) fragment 收到 onDestroy() 時(shí) LiveData 會(huì)自動(dòng)移除觀察者。
我們沒(méi)有做任何事情來(lái)特別是處理配置的變化(如:用戶旋轉(zhuǎn)屏幕)募书。當(dāng)配置發(fā)生變化時(shí) ViewModel 將會(huì)自動(dòng)恢復(fù)轧钓,所以,只要新的 fragment 啟動(dòng)锐膜,它將會(huì)收到屬于 ViewModel 的相同實(shí)例毕箍,并且使用最新的數(shù)據(jù)立即調(diào)用回調(diào)。這就是為什么 ViewModel 不應(yīng)該直接引用 View道盏,ViewModel 可能存活的比 View 的生命周期長(zhǎng)而柑。請(qǐng)參閱 ViewModel 的生命周期。
獲取數(shù)據(jù)
我們已經(jīng)將 ViewModel 鏈接到了 fragment荷逞,但是 ViewModel 怎樣獲取數(shù)據(jù)呢媒咳?這個(gè)例子中,假設(shè)我們的后端提供了一個(gè) REST API种远。我們將會(huì)使用 Retrofit 庫(kù)來(lái)訪問(wèn)后端涩澡,你也可以自由的使用其它庫(kù)來(lái)達(dá)到同樣的目的。
這是 retrofit 的 Webservice 類坠敷,用于和后端通訊:
public interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
一個(gè)簡(jiǎn)單的 ViewModel 實(shí)現(xiàn)可以直接調(diào)用 Webservice 獲取數(shù)據(jù)并將其分配給用戶對(duì)象妙同。即使這樣可以使用射富,但是應(yīng)用程序?qū)?huì)隨著增長(zhǎng)而難以維護(hù)。將太多的職責(zé)交給 ViewModel 這違反了我們前面提到的關(guān)注點(diǎn)分離的原則粥帚。此外胰耗,ViewModel 的作用域依賴于 Activity 或 Fragment 的生命周期,因此在 Activity 或 Fragment 的生命周期結(jié)束時(shí)丟失所有的數(shù)據(jù)是一種不好的用戶體驗(yàn)芒涡。故而柴灯,我們的 ViewModel 將把這項(xiàng)工作委托給一個(gè)新的 Repository 模塊。
<p><strong>Repository</strong> 模塊負(fù)責(zé)處理數(shù)據(jù)操作费尽。它們?yōu)閼?yīng)用程序的其它部分提供了一個(gè)干凈的 API赠群。它們知道在數(shù)據(jù)更新時(shí)從哪里獲取數(shù)據(jù)和調(diào)用哪些 API『涤祝可以將其視為不同數(shù)據(jù)源(持久化模型乎串,Web 服務(wù)苟弛,緩存等)之間的中間層角寸。</p>
下面的 UserRepository 類使用 WebService 來(lái)獲取用戶數(shù)據(jù)。
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// 這是最佳的實(shí)現(xiàn)忿项,下面會(huì)有解釋
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// 為了簡(jiǎn)單起見(jiàn)省略錯(cuò)誤的情況
data.setValue(response.body());
}
});
return data;
}
}
雖然 Repository 模塊看起來(lái)是不必要的闷旧,但是它起著一個(gè)重要的作用长豁;它抽象了應(yīng)用程序其它部分的數(shù)據(jù)源。現(xiàn)在 ViewModel 不知道數(shù)據(jù)是由 Webservice 獲取的忙灼,這意味著可以根據(jù)需求將其切換為其它實(shí)現(xiàn)匠襟。
注:為了簡(jiǎn)單起見(jiàn),我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況该园。有關(guān)于暴露錯(cuò)誤和加載狀態(tài)的可選實(shí)現(xiàn)方式酸舍,請(qǐng)參閱附錄:暴露網(wǎng)絡(luò)狀態(tài)。
管理組件之間的依賴
上面的 UserRepository 類需要一個(gè) Webservice 的實(shí)例來(lái)完成其工作里初】忻悖可以簡(jiǎn)單的創(chuàng)建 Webservice,但是這需要知道 Webservice 的依賴來(lái)構(gòu)造它双妨。這將會(huì)顯著的使代碼復(fù)雜和重復(fù)(例如:需要 Webservice 實(shí)例的每個(gè)類都需要知道如何使用它的依賴來(lái)構(gòu)造它)淮阐。另外,UserRepostory 可能不是唯一需要 Webservice 的類刁品。如果每個(gè)類都創(chuàng)建一個(gè)新的 Webservice泣特,這將會(huì)造成非常大的資源負(fù)擔(dān)。
有兩種模式可以解決這個(gè)問(wèn)題:
依賴注入:依賴注入允許類定義其依賴而不構(gòu)造它們挑随。在運(yùn)行時(shí)状您,另一個(gè)類負(fù)責(zé)提供這些依賴。推薦使用 Google 的 Dagger 2 庫(kù)在 Android 應(yīng)用中實(shí)現(xiàn)依賴注入。Dagger 2 通過(guò)遍歷依賴關(guān)系樹(shù)自動(dòng)構(gòu)建對(duì)象并為依賴提供編譯時(shí)保障膏孟。
服務(wù)定位:服務(wù)定位提供了一個(gè)注冊(cè)表眯分,類可以從中獲取它們的依賴關(guān)系,而不是構(gòu)造它們骆莹。與依賴注入(DI)相比颗搂,服務(wù)定位實(shí)現(xiàn)起來(lái)相對(duì)容易担猛,所以如果不熟悉 DI幕垦,請(qǐng)使用服務(wù)定位代替。
這些模式允許你擴(kuò)展代碼傅联,因?yàn)樗鼈兲峁┣逦哪J絹?lái)管理依賴關(guān)系先改,而不會(huì)重復(fù)代碼或增加復(fù)雜性。兩者都允許替換實(shí)現(xiàn)進(jìn)行測(cè)試蒸走;這是使用它們的主要好處之一仇奶。
在這個(gè)例子中,我們將使用 Dagger 2 來(lái)管理依賴比驻。
連接 ViewModel 和 Repository
修改 UserProfileViewModel 以使用 Repository该溯。
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository 參數(shù)由 Dagger 2 提供
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel 是由 Fragment 創(chuàng)建的
// 所以我們知道 userId 不會(huì)改變
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}
緩存數(shù)據(jù)
上述 Repository 的實(shí)現(xiàn)對(duì)于抽象調(diào)用 Web 服務(wù)是很好的,但是因?yàn)樗鼉H依賴于一個(gè)數(shù)據(jù)源所以不是很實(shí)用别惦。
上述 UserRepository 的實(shí)現(xiàn)的問(wèn)題是在獲取到數(shù)據(jù)之后沒(méi)有把數(shù)據(jù)保存下來(lái)狈茉。如果用戶離開(kāi) UserProfileFragment 然后返回回來(lái),應(yīng)用將會(huì)重新獲取數(shù)據(jù)掸掸。這樣是很不好的氯庆,原因有兩個(gè):一是浪費(fèi)了寶貴的網(wǎng)絡(luò)帶寬;二是迫使用戶等待新的查詢完成扰付。為了解決這個(gè)問(wèn)題堤撵,我們將在 UserRepository 中添加一個(gè)新的數(shù)據(jù)源,用以在內(nèi)存中緩存 User 對(duì)象羽莺。
@Singleton // 告訴 Dagger 這個(gè)類只應(yīng)該構(gòu)造一次
public class UserRepository {
private Webservice webservice;
// 簡(jiǎn)單的內(nèi)存緩存实昨,為了簡(jiǎn)單忽略相關(guān)細(xì)節(jié)
private UserCache userCache;
public LiveData<User> getUser(String userId) {
LiveData<User> cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData<User> data = new MutableLiveData<>();
userCache.put(userId, data);
// 這還不是最好的但比以前的好。
// 一個(gè)完整的實(shí)現(xiàn)必須處理錯(cuò)誤的情況盐固。
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}
持久化數(shù)據(jù)
在當(dāng)前的實(shí)現(xiàn)中屠橄,如果用戶旋轉(zhuǎn)屏幕或離開(kāi)并返回應(yīng)用,已存在的 UI 將會(huì)立即可見(jiàn)闰挡,因?yàn)?Repository 從內(nèi)存緩存中取回?cái)?shù)據(jù)锐墙。但是,如果用戶離開(kāi)應(yīng)用并在幾個(gè)小時(shí)后(Android OS 已經(jīng)殺死進(jìn)程后)返回又會(huì)發(fā)生什么长酗?
如果是目前的實(shí)現(xiàn)溪北,將會(huì)需要再次從網(wǎng)絡(luò)獲取數(shù)據(jù)。這不僅是一個(gè)不好的用戶體驗(yàn),并且也是浪費(fèi)之拨,因?yàn)檫@將會(huì)使用移動(dòng)數(shù)據(jù)重新獲取同樣的數(shù)據(jù)茉继。可以通過(guò)緩存 Web 請(qǐng)求來(lái)簡(jiǎn)單的解決這個(gè)問(wèn)題蚀乔,但是這會(huì)導(dǎo)致新的問(wèn)題烁竭。如果相同的用戶數(shù)據(jù)來(lái)自另一種請(qǐng)求并顯示(例如:獲取朋友列表)將會(huì)怎樣?這時(shí)應(yīng)用會(huì)顯示不一致的數(shù)據(jù)吉挣,這是最令人困惑的用戶體驗(yàn)派撕。例如:相同用戶的數(shù)據(jù)可能會(huì)顯示的不同,因?yàn)榕笥蚜斜淼恼?qǐng)求和用戶個(gè)人信息的請(qǐng)求可能會(huì)在不同的時(shí)間執(zhí)行睬魂。應(yīng)用程序需要合并它們以避免顯示不一致的數(shù)據(jù)终吼。
解決這個(gè)問(wèn)題的正確方法是使用持久化模型。這就是持久化庫(kù) Room 的用武之地了氯哮。
<p><strong><a >Room</a></strong> 是一個(gè)以最少的樣板代碼提供本地?cái)?shù)據(jù)持久化的對(duì)象映射庫(kù)际跪。在編譯時(shí),它會(huì)根據(jù)模式驗(yàn)證每個(gè)查詢喉钢,所以損壞的 SQL 查詢只會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤姆打,而不是運(yùn)行時(shí)崩潰。Room 抽象出一些使用原始 SQL 表查詢的底層實(shí)現(xiàn)細(xì)節(jié)肠虽。它還允許觀察數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢)的變化幔戏,并通過(guò) <em>LiveData</em> 對(duì)象暴露這些變化。另外舔痕,它明確定義了線程約束以解決常見(jiàn)問(wèn)題(如在主線程訪問(wèn)存儲(chǔ))评抚。</p>
注:如果你熟悉其它的持久化解決方案,如:SQLite ORM 或像 Realm 等其他的數(shù)據(jù)庫(kù)伯复,你不需要用更換為 Room慨代,除非 Room 的功能對(duì)你的用例更加適用。
使用 Room啸如,需要定義我們的局部模式侍匙。首先,用 @Entity 注釋 User 類叮雳,將其標(biāo)記為數(shù)據(jù)庫(kù)中的一個(gè)表想暗。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// 字段的 get 和 set 方法
}
然后,通過(guò)繼承 RoomDatabase 為應(yīng)用創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)帘不。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
請(qǐng)注意说莫,MyDatabase 是抽象類,Room 會(huì)自動(dòng)提供其實(shí)現(xiàn)類寞焙。詳細(xì)信息請(qǐng)參閱 Room 的文檔储狭。
現(xiàn)在需要一種方法來(lái)將用戶數(shù)據(jù)插入數(shù)據(jù)庫(kù)互婿。為此,我們將創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象( DAO )辽狈。
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
然后慈参,在數(shù)據(jù)庫(kù)類中引用 DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請(qǐng)注意刮萌,load 方法的返回值是 LiveData<User>驮配。Room 知道數(shù)據(jù)庫(kù)何時(shí)被修改,當(dāng)數(shù)據(jù)發(fā)生變化時(shí)着茸,它會(huì)通知所有處于活動(dòng)狀態(tài)的觀察者壮锻。使用 LiveData 是高效的是因?yàn)樗挥性谥辽儆幸粋€(gè)處于活動(dòng)狀態(tài)的觀察者時(shí)才會(huì)更新數(shù)據(jù)。
注:從 alpha 1 版本開(kāi)始元扔,Room 基于表修改的檢查無(wú)效躯保,這意味著它可能會(huì)發(fā)送錯(cuò)誤的通知旋膳。
現(xiàn)在修改 UserRepository 來(lái)整合 Room 的數(shù)據(jù)源澎语。
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData<User> getUser(String userId) {
refreshUser(userId);
// 直接從數(shù)據(jù)庫(kù)返回一個(gè) LiveData。
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// 在后臺(tái)線程中運(yùn)行
// 檢查最近是否獲取過(guò) user
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// 刷新數(shù)據(jù)
Response response = webservice.getUser(userId).execute();
// TODO 檢查錯(cuò)誤等验懊。
// 更新數(shù)據(jù)庫(kù)擅羞。LiveData 將會(huì)自動(dòng)刷新
// 所以除了更新數(shù)據(jù)庫(kù)外不需要任何操作。
userDao.save(response.body());
}
});
}
}
請(qǐng)注意义图,即使我們更改了 UserRepository 中的數(shù)據(jù)來(lái)源减俏,我們也不需要更改 UserProfileViewModel 或 UserProfileFragment。這是抽象帶來(lái)的靈活性碱工。這樣也非常易于測(cè)試娃承,因?yàn)樵跍y(cè)試 UserProfileViewModel 可以提供一個(gè)假的 UserRepository。
現(xiàn)在我們的代碼是完整的怕篷,如果用戶日后再回到相同的 UI历筝,他們會(huì)立即看到用戶信息,因?yàn)槲覀円呀?jīng)將其持久化了廊谓。同時(shí)梳猪,如果數(shù)據(jù)過(guò)期,Repository 將會(huì)在后臺(tái)更新數(shù)據(jù)蒸痹。當(dāng)然春弥,根據(jù)你的用例,如果持久化的數(shù)據(jù)太舊你可能不希望顯示它們叠荠。
在一些用例中匿沛,如下拉刷新,在進(jìn)行網(wǎng)絡(luò)操作時(shí)顯示用戶數(shù)據(jù)對(duì)于 UI 來(lái)說(shuō)非常重要榛鼎。將 UI 操作從實(shí)際數(shù)據(jù)中分離是一個(gè)很好的做法逃呼,因?yàn)槠淇赡苡捎诟鞣N原因而被更新(例如:如果獲取一個(gè)朋友列表公给,可能會(huì)再次獲取到相同的 user 并觸發(fā) LiveData<User> 更新)。從 UI 的角度來(lái)看蜘渣,動(dòng)態(tài)請(qǐng)求實(shí)際上只是另一個(gè)數(shù)據(jù)點(diǎn)淌铐,類似其它任何數(shù)據(jù)片段(如:User 對(duì)象)
這種用例有兩種常見(jiàn)的解決方案:
更改 getUser 以返回包含網(wǎng)絡(luò)操作狀態(tài)的 LiveData。在附錄:暴露網(wǎng)絡(luò)狀態(tài)部分提供了一個(gè)實(shí)現(xiàn)例子蔫缸。
在 Repository 類中提供一個(gè)可以返回 User 刷新?tīng)顟B(tài)的公共方法腿准。如果只是為了響應(yīng)明確的用戶操作而在 UI 中顯示網(wǎng)絡(luò)狀態(tài)(如:下拉刷新),則這種方式是更好的選擇拾碌。
單一數(shù)據(jù)源
不同的 REST API 接口返回相同的數(shù)據(jù)是很常見(jiàn)的吐葱。例如,如果后端有另一個(gè)返回朋友列表的接口校翔,相同的用戶對(duì)象(也許是不同的粒度)可能來(lái)自兩個(gè)不同的 API 接口弟跑。如果 UserRepository 把從 Webservice 請(qǐng)求獲取到的響應(yīng)原樣返回,碼么 UI 可能會(huì)顯示不一致的數(shù)據(jù)防症,因?yàn)樵谶@些請(qǐng)求之間數(shù)據(jù)可能在服務(wù)端發(fā)生了改變孟辑。這就是為什么在 UserRepository 的實(shí)現(xiàn)中 Web 服務(wù)的回調(diào)只是將數(shù)據(jù)保存到了數(shù)據(jù)庫(kù)。然后蔫敲,對(duì)數(shù)據(jù)庫(kù)的更改將會(huì)觸發(fā)處于活動(dòng)狀態(tài)的 LiveData 對(duì)象上的回調(diào)饲嗽。
在這個(gè)模型中,數(shù)據(jù)庫(kù)服務(wù)作為單一數(shù)據(jù)源奈嘿,應(yīng)用程序的其它部分通過(guò) Repository 來(lái)訪問(wèn)它貌虾。無(wú)論是否是否磁盤緩存,建議 Repository 指定一個(gè)數(shù)據(jù)源作為應(yīng)用程序其它部分的單一數(shù)據(jù)源裙犹。
測(cè)試
我們已經(jīng)提到分離的好處之一是可測(cè)試性尽狠。讓我們看看如何測(cè)試每個(gè)代碼模塊。
用戶界面或用戶交互:這將是唯一一次需要 Android UI Instrumentation test叶圃。測(cè)試 UI 代碼的最佳方式是創(chuàng)建一個(gè) Espresso 測(cè)試袄膏。可以創(chuàng)建一個(gè) fragment 并為其提供一個(gè)模擬的 ViewModel盗似。因?yàn)?fragment 只和 ViewModel 交互哩陕,隨意模擬 ViewModel 足以完全測(cè)是 UI。
ViewModel:可以使用 JUnit test 來(lái)測(cè)試 ViewModel赫舒。只需要模擬 UserRepository 來(lái)測(cè)試它悍及。
UserRepository:也可以使用 JUnit test 來(lái)測(cè)試 UserRepository。需要模擬 Webservice 和 DAO接癌⌒母希可以測(cè)試 UserRepository 是否進(jìn)行了正確的 Web 服務(wù)調(diào)用,將結(jié)構(gòu)保存到數(shù)據(jù)庫(kù)缺猛,如果數(shù)據(jù)被緩存且是最新的缨叫,則不會(huì)發(fā)起任何不必要的請(qǐng)求椭符。因?yàn)?WebService 和 UserDao 都是接口,所以可以模擬它們或者為更復(fù)雜的測(cè)試用例創(chuàng)建假的實(shí)現(xiàn)耻姥。
-
UserDao:推薦使用 Instrumentation 測(cè)試的方式測(cè)試 DAO 類销钝。因?yàn)?Instrumentation 測(cè)試不需要任何 UI,它們會(huì)運(yùn)行的很快琐簇。對(duì)于每個(gè)測(cè)試蒸健,可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù)以確保測(cè)試沒(méi)有任何副作用(例如:改變磁盤上的數(shù)據(jù)庫(kù)文件)。
Room 還允許指定數(shù)據(jù)庫(kù)實(shí)現(xiàn)婉商,所以可以通過(guò)向其提供 SupportSQLiteOpenHelper 的 JUnit 實(shí)現(xiàn)來(lái)測(cè)試它似忧。通常不推薦這種方式,因?yàn)樵O(shè)備上運(yùn)行的 SQLite 版本可能和主機(jī)上的 SQLite 版本不同丈秩。
Webservice:重點(diǎn)是使測(cè)試相對(duì)于外部獨(dú)立盯捌,所以 Webservice 的測(cè)試要避免通過(guò)網(wǎng)絡(luò)調(diào)用后端。有許多庫(kù)可以幫助完成該測(cè)試蘑秽。例如:MockWebServer 是一個(gè)很好的庫(kù)饺著,可以幫助為測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)。
-
測(cè)試工件架構(gòu)組件提供了一個(gè) maven 工件來(lái)控制其后臺(tái)線程筷狼。在 android.arch.core:core-testing 工件中瓶籽,有兩個(gè) JUnit 規(guī)則:
InstantTaskExecutorRule:該規(guī)則可用于強(qiáng)制架構(gòu)組件立即執(zhí)行調(diào)用線程上的任何后臺(tái)操作匠童。
CountingTaskExecutorRule:該規(guī)則可用于 Instrumentation 測(cè)試埂材,以等待架構(gòu)組件的后臺(tái)操作,或?qū)⑵溥B接到 Espresso 作為閑置資源汤求。
最終的架構(gòu)
下圖顯示了推薦架構(gòu)中的所有模塊以及它們?nèi)绾位ハ嘟换ィ?/p>
指導(dǎo)原則
編程是一個(gè)創(chuàng)作領(lǐng)域俏险,構(gòu)建 Android 應(yīng)用也不例外。有許多方法來(lái)解決問(wèn)題扬绪,無(wú)論是在多個(gè) activity 或 fragment 之間傳遞數(shù)據(jù)竖独,是獲取遠(yuǎn)程數(shù)據(jù)并為了離線模式將其持久化到本地,還是特殊應(yīng)用遭遇的其它常見(jiàn)情況挤牛。
雖然一下建議不是強(qiáng)制性的莹痢,但是以我們的經(jīng)驗(yàn),從長(zhǎng)遠(yuǎn)來(lái)看墓赴,遵循這些建議將會(huì)使代碼庫(kù)更健壯竞膳,易測(cè)試和易維護(hù)。
在 manifest 中定義的入口點(diǎn)诫硕,如:acitivy坦辟,fragment,broadcast receiver 等章办,不是數(shù)據(jù)源锉走。相反滨彻,它們應(yīng)該只是協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集。由于每個(gè)應(yīng)用程序組件的存活時(shí)間很短挪蹭,這取決于用戶與其設(shè)備的交互以及運(yùn)行時(shí)的總體狀況亭饵,所以任何入口點(diǎn)都不應(yīng)該成為數(shù)據(jù)源。
嚴(yán)格的在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確的責(zé)任界限梁厉。例如:不在代碼庫(kù)中的多個(gè)類或包中擴(kuò)散從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼冬骚。同樣,不要將無(wú)關(guān)的責(zé)任(如:數(shù)據(jù)緩存和數(shù)據(jù)綁定)放到同一個(gè)類中懂算。
每個(gè)模塊盡可能少的暴露出來(lái)只冻。不要視圖創(chuàng)建暴露模塊內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的“只一個(gè)”的快捷方式。你可能會(huì)在短期內(nèi)節(jié)省一些時(shí)間计技,但是隨著代碼庫(kù)的發(fā)展喜德,你將會(huì)多次償還更多的基數(shù)債務(wù)。
當(dāng)定義模塊間的交互時(shí)垮媒,請(qǐng)考慮如何讓每個(gè)模塊可以獨(dú)立的測(cè)試舍悯。例如,擁有一個(gè)用于從網(wǎng)絡(luò)獲取數(shù)據(jù)且定義良好的 API 的模塊睡雇,將會(huì)使其更易于測(cè)試在本地?cái)?shù)據(jù)庫(kù)中持久化數(shù)據(jù)萌衬。相反,如果將兩個(gè)模塊的邏輯放在一個(gè)地方它抱,或者將網(wǎng)絡(luò)代碼擴(kuò)散到整個(gè)代碼庫(kù)秕豫,測(cè)試將會(huì)變的非常困難(并非不可能)。
應(yīng)用程序的核心是使其脫穎而出观蓄。不要花費(fèi)時(shí)間重復(fù)造輪子或一次又一次的編寫(xiě)相同的樣板代碼混移。相反,將精力集中在使應(yīng)用程序獨(dú)一無(wú)二上侮穿,讓 Android Architecture Components 和其它的優(yōu)秀的庫(kù)來(lái)處理重復(fù)的樣板代碼歌径。
持久化盡可能多的相關(guān)最新數(shù)據(jù),以便應(yīng)用程序在設(shè)備處于離線模式時(shí)還可以使用亲茅。即使你可以享用穩(wěn)定高速的網(wǎng)絡(luò)連接回铛,但是你的用戶可能無(wú)法享用。
Repository 應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一數(shù)據(jù)源克锣。每當(dāng)應(yīng)用程序需要訪問(wèn)數(shù)據(jù)時(shí)茵肃,數(shù)據(jù)應(yīng)該始終來(lái)源于單一數(shù)據(jù)源。有關(guān)更多信息娶耍,請(qǐng)參閱單一數(shù)據(jù)源
附錄:暴露網(wǎng)絡(luò)狀態(tài)
在上面推薦的應(yīng)用程序架構(gòu)部分免姿,為了保持示例簡(jiǎn)單我們故意忽略網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)。在本節(jié)中榕酒,我們演示一種通過(guò) Resource 類暴露網(wǎng)絡(luò)狀態(tài)來(lái)封裝數(shù)據(jù)和其狀態(tài)胚膊。
以下是一個(gè)實(shí)現(xiàn)的例子:
// 描述數(shù)據(jù)和其狀態(tài)的類
public class Resource<T> {
@NonNull public final Status status;
@Nullable public final T data;
@Nullable public final String message;
private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(@NonNull T data) {
return new Resource<>(SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}
因?yàn)閺拇疟P中獲取并顯示數(shù)據(jù)同時(shí)再?gòu)木W(wǎng)絡(luò)獲取數(shù)據(jù)是一種常見(jiàn)的用例故俐。我們將創(chuàng)建一個(gè)可以在多個(gè)地方使用的幫助類 NetworkBoundResource。下面是 NetworkBoundResource 的決策樹(shù)紊婉。
它通過(guò)觀察資源的數(shù)據(jù)庫(kù)药版。當(dāng)首次從數(shù)據(jù)庫(kù)加載條目時(shí),NetworkBoundResource 檢查返回結(jié)果是否足夠好可以被發(fā)送和(或)應(yīng)該從網(wǎng)絡(luò)獲取數(shù)據(jù)喻犁。請(qǐng)注意槽片,他們可能同時(shí)發(fā)生,因?yàn)槟憧赡軙?huì)希望在顯示緩存數(shù)據(jù)的同時(shí)從網(wǎng)絡(luò)更新數(shù)據(jù)肢础。
如果網(wǎng)絡(luò)調(diào)用成功还栓,則將返回?cái)?shù)據(jù)保存到數(shù)據(jù)庫(kù)中并重新初始化數(shù)據(jù)流。如果網(wǎng)絡(luò)請(qǐng)求失敗传轰,直接發(fā)送一個(gè)錯(cuò)誤剩盒。
注:將新的數(shù)據(jù)保存到磁盤后,要從數(shù)據(jù)庫(kù)重新初始化數(shù)據(jù)流慨蛙,但是通常不需要這樣做辽聊,因?yàn)閿?shù)據(jù)庫(kù)將會(huì)發(fā)送變更。另一方面期贫,依賴數(shù)據(jù)庫(kù)發(fā)送變更會(huì)有一些不好的副作用跟匆,因?yàn)樵跀?shù)據(jù)沒(méi)有變化時(shí)如果數(shù)據(jù)庫(kù)會(huì)避免發(fā)送更改將會(huì)使其中斷。我們也不希望發(fā)送從網(wǎng)絡(luò)返回的結(jié)果通砍,因?yàn)檫@違背的單一數(shù)據(jù)源原則(即使在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變保存值)玛臂。我們也不希望在沒(méi)有新數(shù)據(jù)的時(shí)候發(fā)送 SUCCESS,因?yàn)檫@會(huì)給客戶端發(fā)送錯(cuò)誤信息埠帕。
以下是 NetworkBoundResource 類為其子類提供的公共 API:
// ResultType: Resource 數(shù)據(jù)的類型
// RequestType: API 響應(yīng)的類型
public abstract class NetworkBoundResource<ResultType, RequestType> {
// 調(diào)用該方法將 API 響應(yīng)的結(jié)果保存到數(shù)據(jù)庫(kù)中垢揩。
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// 調(diào)用該方法判斷數(shù)據(jù)庫(kù)中的數(shù)據(jù)是否應(yīng)該從網(wǎng)絡(luò)獲取并更新。
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// 調(diào)用該方法從數(shù)據(jù)庫(kù)中獲取緩存數(shù)據(jù)敛瓷。
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// 調(diào)用該方法創(chuàng)建 API 請(qǐng)求。
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// 獲取失敗時(shí)調(diào)用斑匪。
// 子類可能需要充值組件(如:速率限制器)呐籽。
@MainThread
protected void onFetchFailed() {
}
// 返回一個(gè)代表 Resource 的 LiveData。
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
請(qǐng)注意蚀瘸,上述類定義了兩個(gè)類型參數(shù)(ResultType狡蝶,RequestType),因?yàn)閺?API 返回的數(shù)據(jù)類型可能和本地使用的數(shù)據(jù)類型不同贮勃。
還要注意贪惹,上述代碼使用 ApiResponse 作為網(wǎng)絡(luò)請(qǐng)求,ApiResponse 是對(duì)于 Retrofit2.Call 類的簡(jiǎn)單封裝寂嘉,用以將其響應(yīng)轉(zhuǎn)換為 LiveData奏瞬。
以下是 NetworkBoundResource 類的其余實(shí)現(xiàn)部分枫绅。
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// 重新附加 dbSource 作為新的來(lái)源,
// 它將會(huì)迅速發(fā)送最新的值。
result.addSource(dbSource,
newData -> result.setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (response.isSuccessful()) {
saveResultAndReInit(response);
} else {
onFetchFailed();
result.addSource(dbSource,
newData -> result.setValue(
Resource.error(response.errorMessage, newData)));
}
});
}
@MainThread
private void saveResultAndReInit(ApiResponse<RequestType> response) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
saveCallResult(response.body);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// 指定請(qǐng)求一個(gè)最新的實(shí)時(shí)數(shù)據(jù)硼端。
// 否則并淋,會(huì)得到最新的緩存數(shù)據(jù),并且可能不會(huì)由從網(wǎng)絡(luò)獲取的最新數(shù)據(jù)更新珍昨。
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}
現(xiàn)在县耽,可以使用 NetworkBoundResource 在 Repository 中編寫(xiě)磁盤和網(wǎng)絡(luò)綁定 User 的實(shí)現(xiàn)。
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final String userId) {
return new NetworkBoundResource<User,User>() {
@Override
protected void saveCallResult(@NonNull User item) {
userDao.insert(item);
}
@Override
protected boolean shouldFetch(@Nullable User data) {
return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
}
@NonNull @Override
protected LiveData<User> loadFromDb() {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData<ApiResponse<User>> createCall() {
return webservice.getUser(userId);
}
}.getAsLiveData();
}
}