本指南適用于那些已經(jīng)擁有開發(fā)Android應(yīng)用基礎(chǔ)知識(shí)的開發(fā)人員兽掰,現(xiàn)在想了解能夠開發(fā)出更加健壯桥氏、優(yōu)質(zhì)的應(yīng)用程序架構(gòu)。
首先需要說明的是:Android Architecture Components 翻譯為 Android架構(gòu)組件 并不是我自己隨意翻譯的,而是Google官方博客中明確稱其為 Android架構(gòu)組件惨奕,因此我遵循了這種叫法。
下面這張圖是Android架構(gòu)組件完整的架構(gòu)圖纽竣,其中表示了的架構(gòu)組件的所有模塊以及它們之間如何交互:
APP開發(fā)者面臨的常見問題
與傳統(tǒng)的桌面應(yīng)用程序不同墓贿,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多,在大多數(shù)情況下蜓氨,它們只在桌面快捷啟動(dòng)方式中有一個(gè)入口聋袋,并且作為單個(gè)進(jìn)程運(yùn)行。一個(gè)典型的Android應(yīng)用程序是由多個(gè) APP組件(Android四大組件) 構(gòu)成的穴吹,包括 activities, fragments, services, content providers and broadcast receivers幽勒。
這些 app組件 中的大部分都是在 應(yīng)用清單(AndroidManifast.xml)中聲明的,Android操作系統(tǒng)使用這些組件將應(yīng)用程序集成到設(shè)備的用戶界面中港令。雖然啥容,應(yīng)用程序通常上是以單個(gè)進(jìn)程運(yùn)行的,但是一個(gè)合理的Android應(yīng)用需要更加靈活顷霹,因?yàn)橛脩艨梢酝ㄟ^不同的應(yīng)用程序咪惠,在他們的設(shè)備上不斷切換流程和任務(wù)。
想象下在我們最喜愛的社交網(wǎng)絡(luò)應(yīng)用中分享照片時(shí)會(huì)發(fā)生什么情況淋淀。首先這個(gè)應(yīng)用程序觸發(fā)一個(gè)Camera(拍照或攝像) Intent遥昧,由Android操作系統(tǒng)啟動(dòng)一個(gè)Camera應(yīng)用來處理請(qǐng)求。此時(shí)朵纷,用戶雖然離開了這個(gè)社交網(wǎng)絡(luò)應(yīng)用炭臭,但他們的體驗(yàn)是無縫的。相機(jī)應(yīng)用程序又可能觸發(fā)其他 Intent袍辞,例如啟動(dòng)文件管理器鞋仍,該文件管理器可以啟動(dòng)另一個(gè)應(yīng)用程序,最終用戶回到社交網(wǎng)絡(luò)應(yīng)用并分享照片搅吁。此外威创,用戶在這個(gè)過程的任何時(shí)候都可能被電話打斷落午,并在打完電話后回來繼續(xù)分享照片。
在Android中那婉,這種應(yīng)用程序跳轉(zhuǎn)行為是很常見的板甘,所以我們的應(yīng)用程序必須正確處理這些流程。請(qǐng)記住详炬,移動(dòng)設(shè)備是資源受限的盐类,所以在任何時(shí)候呛谜,操作系統(tǒng)都可能需要?dú)⑺酪恍?yīng)用程序在跳,以騰出空間給新的應(yīng)用。
這一切的要點(diǎn)在于隐岛,我們的 app組件 可以單獨(dú)和無序地啟動(dòng)猫妙,并且可以在任何時(shí)候由用戶或系統(tǒng)銷毀。由于 app組件 是短暫的聚凹,并且它們的生命周期(創(chuàng)建和銷毀時(shí))不在我們的控制之下割坠,因此我們不應(yīng)該在app組件中存儲(chǔ)任何 app數(shù)據(jù)或狀態(tài),并且 app組件不應(yīng)相互依賴妒牙。
通用架構(gòu)原則
如果不使用 app組件存儲(chǔ)app數(shù)據(jù)和狀態(tài)彼哼,那該如何構(gòu)造應(yīng)用程序呢?
我們需要關(guān)注的最重要的事情是:如何在你的應(yīng)用中分離關(guān)注點(diǎn)湘今。最常見的錯(cuò)誤是將所有的代碼寫入一個(gè) Activity 或 Fragment敢朱,任何不處理 UI 或 與操作系統(tǒng)交互的代碼都不應(yīng)該出現(xiàn)在這些類中,我們應(yīng)該盡可能保持 Activity 或Fragment 精簡(jiǎn)摩瞎,這樣可以避免許多生命周期相關(guān)的問題拴签。請(qǐng)記住,我們不擁有這些類旗们,它們只是建立操作系統(tǒng)和我們的應(yīng)用程序之間契約的膠水類蚓哩。Android操作系統(tǒng)可能會(huì)隨時(shí)根據(jù)用戶交互或其他因素(如低內(nèi)存)來銷毀它們,最好盡可能地減少依賴他們上渴,以提供可靠的用戶體驗(yàn)岸梨。
第二個(gè)重要原則是: 你應(yīng)該從一個(gè) Model 驅(qū)動(dòng)你的UI,最好是一個(gè)持久化的 Model驰贷。之所以說持久化是理想的 Model,原因有兩個(gè):如果操作系統(tǒng)銷毀你的應(yīng)用程序以釋放資源洛巢,那么你的用戶就不會(huì)丟失數(shù)據(jù)括袒,即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,你的應(yīng)用程序也會(huì)繼續(xù)工作稿茉。Model 是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件锹锰,它們獨(dú)立于應(yīng)用程序的 Views 和 app組件芥炭,因此 Model 與這些 app組件的生命周期問題是相隔離的。保持簡(jiǎn)潔的UI代碼恃慧,以及不受約束的應(yīng)用程序邏輯园蝠,可以使app的管理更加容易,基于具有明確定義的管理數(shù)據(jù)責(zé)任的模型類的應(yīng)用程序痢士,會(huì)更加具有可測(cè)試性彪薛,并使我們的應(yīng)用程序狀態(tài)保持前后一致。
推薦的App架構(gòu)
在本節(jié)中怠蹂,我們將演示如何通過使用用例來構(gòu)造使用了 架構(gòu)組件(Architecture Components) 的應(yīng)用程序善延。
注意:不可能有一種編寫應(yīng)用程序的方法對(duì)每個(gè)場(chǎng)景都是最好的。對(duì)于大多數(shù)用例來說城侧,推薦的這個(gè)架構(gòu)可能是一個(gè)好的起點(diǎn)易遣。如果你已經(jīng)有了編寫Android應(yīng)用的好方法,那就不要在更改了嫌佑。
假如我沒正在搭建一個(gè)用來顯示 用戶概況的UI豆茫,該用戶概況將使用 REST API 從我們自己的服務(wù)器端獲取。
搭建用戶界面
這個(gè)UI 將由 UserProfileFragment.java
及 Fragment 相應(yīng)的 user_profile_layout.xml
布局文件組成屋摇。
為了驅(qū)動(dòng)用戶界面揩魂,我們的數(shù)據(jù)模型需要保存兩個(gè)數(shù)據(jù)元素。
用戶ID:用戶的標(biāo)識(shí)符摊册。最好使用 fragment 參數(shù)(
setArguments
方法) 將此信息傳遞到 fragment 中肤京。如果Android系統(tǒng)銷毀了你的進(jìn)程,這些信息將被保留茅特,便于應(yīng)用在下次重新啟動(dòng)時(shí)可用忘分。用戶對(duì)象:保存用戶數(shù)據(jù)的 POJO(簡(jiǎn)單的Java對(duì)象)。
我們將創(chuàng)建一個(gè)基于ViewModel 的 UserProfileViewModel
類來保存這些信息白修。
一個(gè) ViewModel 提供了一個(gè)特定 UI 組件中的數(shù)據(jù)妒峦,如一個(gè) fragment 或 activity, 并且負(fù)責(zé)與數(shù)據(jù)處理業(yè)務(wù)的通信,例如調(diào)用其他 app組件 來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶信息的修改兵睛。ViewModel不知道View肯骇,并且不受配置更改的影響,例如由于屏幕旋轉(zhuǎn)而重新創(chuàng)建 Activity祖很。
現(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)單起見,省略了布局文件):
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 Fragment {
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);
}
}
現(xiàn)在笨鸡,我們?nèi)绾螌⑺鼈兟?lián)系在一起呢姜钳?當(dāng)給 ViewModel 的 User
字段設(shè)值后坦冠,我們需要一種方法來通知用戶界面,這就是 LiveData 類的作用哥桥。
LiveData 是一個(gè)可觀察的數(shù)據(jù)持有者辙浑。它允許應(yīng)用程序中的組件觀察 LiveData 對(duì)象的更改,但不會(huì)在它們之間創(chuàng)建明確的和嚴(yán)格的依賴關(guān)系路徑拟糕。 LiveData 還會(huì)關(guān)聯(lián) app組件(activities, fragments, services) 的生命周期狀態(tài)判呕,并做出合適的事情來防止內(nèi)存泄漏。
注意:如果你已經(jīng)在使用類似 RxJava 或 Agera 的庫(kù) 佛玄,則可以繼續(xù)使用它們而不是LiveData。但是累澡,當(dāng)你使用它們或其他方式時(shí)梦抢,請(qǐng)確保正確處理生命周期,以便在相關(guān)的LifecycleOwner 停止時(shí)暫停數(shù)據(jù)流愧哟,并在銷毀 LifecycleOwner 時(shí)銷毀數(shù)據(jù)流奥吩。你還可以添加 android.arch.lifecycle:reactivestreams 以將 LiveData 與其他的響應(yīng)流庫(kù)(例如RxJava2)一起使用。
現(xiàn)在我們用 LiveData<User>
替換 UserProfileViewModel
中的 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;
}
}
現(xiàn)在我們修改 UserProfileFragment
以便觀察數(shù)據(jù)并更新 UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
每次更新用戶數(shù)據(jù)時(shí)甘改, 都會(huì)調(diào)用 onChanged
回調(diào)旅东,并刷新 UI。
如果你熟悉其他 可觀察回調(diào)的庫(kù)十艾,你可能已經(jīng)意識(shí)到抵代,我們沒有重寫 fragment 的 onStop()
方法來停止觀察數(shù)據(jù)。這對(duì)于 LiveData 來說是沒有必要的忘嫉,因?yàn)樗巧芷诟兄幕珉梗@意味著它不會(huì)調(diào)用回調(diào),除非 Fragment 處于 活動(dòng)狀態(tài)(已收到 onStart()
但未收到 onStop()
)庆冕。當(dāng) fragment 收到 onDestroy()
時(shí)康吵,LiveData也將自動(dòng)移除觀察者 。
對(duì)于配置變化(例如访递,用戶旋轉(zhuǎn)屏幕)我們也沒有做任何特殊的處理晦嵌。當(dāng)配置改變時(shí),ViewModel 會(huì)自動(dòng)恢復(fù),所以一旦新的 Fragment 生效耍铜,它將接收到相同的 ViewModel實(shí)例,并且 ViewModel 的回調(diào)將立即被當(dāng)前數(shù)據(jù)調(diào)用跌前,這就是 ViewModels 為什么不應(yīng)該直接引用 Views 的原因棕兼,他們可以比 View的生命周期更持久。想了解更多信息的請(qǐng)查看 The lifecycle of a ViewModel 抵乓。
獲取數(shù)據(jù)
現(xiàn)在我們已經(jīng)將 ViewModel 關(guān)聯(lián)到了 Fragment伴挚,但是 ViewModel 如何獲取用戶數(shù)據(jù)呢?在這個(gè)例子中灾炭,我們假設(shè)服務(wù)器端提供了一個(gè) REST API茎芋。我們將使用 Retrofit 庫(kù)來訪問我們的服務(wù)器端,雖然你可以自由使用不同的庫(kù)來達(dá)到同樣的目的蜈出。
下面是retrofit 的 Webservice 当纱,負(fù)責(zé)與服務(wù)器端進(jìn)行通信:
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);
}
ViewModel 的一個(gè)簡(jiǎn)單實(shí)現(xiàn)是直接調(diào)用 Webservice
來獲取數(shù)據(jù)并將其 賦值給 user
對(duì)象微姊,雖然這樣是可行的,但是我們的應(yīng)用程序以后將很難維護(hù)。它賦予了 ViewModel
類太多的職責(zé)库糠,違背了我們前面提到的關(guān)注點(diǎn)分離原則。此外脸爱,ViewModel 的作用域與一個(gè) Activity 或一個(gè) Fragment 生命周期相關(guān)聯(lián)狞玛,當(dāng)他們的生命周期完成時(shí)將丟失所有的數(shù)據(jù),這是非常糟糕的用戶體驗(yàn)卵洗。因此请唱,我們將 ViewModel 的這個(gè)工作委托給了一個(gè)新的模塊 Repository 。
Repository 模塊負(fù)責(zé)數(shù)據(jù)處理操作过蹂。他們?yōu)閼?yīng)用的其余部分提供了一個(gè)干凈的API十绑,他們知道從何處獲取數(shù)據(jù)以及在更新數(shù)據(jù)時(shí)調(diào)用哪些API。你可以將它們視為不同數(shù)據(jù)源 (持久化模型榴啸, web服務(wù), 緩存, etc.)之間的中介孽惰。
UserRepository
類使用 WebService
來獲取用戶數(shù)據(jù)項(xiàng),如下:
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This is not an optimal implementation, we'll fix it below
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// error case is left out for brevity
data.setValue(response.body());
}
});
return data;
}
}
雖然 repository 模塊看起來沒有必要鸥印,但是它有一個(gè)重要的目的勋功,它從應(yīng)用程序的其余部分提取數(shù)據(jù)源。現(xiàn)在我們的 ViewModel 不知道數(shù)據(jù)是從 Webservice 獲取到的库说,這意味著我們可以根據(jù)需要狂鞋,將它(Webservice)替換為其他的實(shí)現(xiàn)。
注意:為了簡(jiǎn)單起見潜的,我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況骚揍。對(duì)于暴露錯(cuò)誤和加載狀態(tài)的另一個(gè)實(shí)現(xiàn),請(qǐng)查看 附錄:暴露網(wǎng)絡(luò)狀態(tài)。
管理組件之間的依賴關(guān)系:
上面的 UserRepository
類需要一個(gè) Webservice 的實(shí)例來工作信不,UserRepository
可以簡(jiǎn)單地創(chuàng)建Webservice 嘲叔,但要做到這一點(diǎn),它必須需要知道 Webservice 類的依賴關(guān)系來構(gòu)造它抽活,這會(huì)使代碼顯著和成倍的復(fù)雜化(例如硫戈,每個(gè)需要 Webservice實(shí)例的類 都需要知道如何用它的依賴來構(gòu)造它)。另外下硕,UserRepository 可能不是唯一需要 Webservice 的類丁逝。如果每個(gè)類創(chuàng)建一個(gè)新的 WebService,這將是非常沉重的資源梭姓。
現(xiàn)在我們有兩種模式可以用來解決這個(gè)問題:
- 依賴注入:依賴注入允許類在不構(gòu)造它們的情況下定義它們的依賴關(guān)系霜幼。在運(yùn)行時(shí),另一個(gè)類負(fù)責(zé)提供這些依賴關(guān)系誉尖。我們推薦 Google 的 Dagger 2 庫(kù)罪既,在Android應(yīng)用中實(shí)現(xiàn)依賴注入。Dagger 2 通過遍歷依賴關(guān)系樹來自動(dòng)構(gòu)造對(duì)象铡恕,并為依賴關(guān)系提供編譯時(shí)間保證萝衩。
- 服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊(cè)表,這個(gè)類可以獲得它們的依賴 而不是 構(gòu)建它們没咙。實(shí)現(xiàn)起來比依賴注入(DI)更容易猩谊,所以如果你不熟悉DI,可以使用 Service Locator祭刚。
這些模式允許您擴(kuò)展代碼牌捷,因?yàn)樗鼈兲峁┝擞糜诠芾硪蕾囮P(guān)系的清晰模式,無需復(fù)制代碼或增加復(fù)雜性涡驮。這兩個(gè)模式也允許交換實(shí)現(xiàn)測(cè)試暗甥, 這是使用它們的主要好處之一。
在這個(gè)例子中捉捅,我們將使用 依賴注入 來管理依賴關(guān)系撤防。
關(guān)聯(lián)ViewModel和repository
現(xiàn)在我們修改 UserProfileViewModel
使用的 repository。
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository parameter is provided by Dagger 2
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel is created per Fragment so
// we know the userId won't change
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)樗灰蕾囉谝粋€(gè)數(shù)據(jù)源寄月,所以它不是很有用。
UserRepository 實(shí)現(xiàn)的問題是无牵,在獲取數(shù)據(jù)之后漾肮,它不保存在任何地方。如果用戶離開 UserProfileFragment 并返回茎毁,應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)克懊。這是不好的,原因有兩個(gè):浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬并強(qiáng)制用戶等待新的查詢完成。為了解決這個(gè)問題谭溉,我們將添加一個(gè)新的數(shù)據(jù)源到 UserRepository 墙懂,這個(gè)數(shù)據(jù)源可以將 User 對(duì)象 緩存 到內(nèi)存中。
@Singleton // informs Dagger that this class should be constructed once
public class UserRepository {
private Webservice webservice;
// simple in memory cache, details omitted for brevity
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);
// this is still suboptimal but better than before.
// a complete implementation must also handle the error cases.
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)屏幕或離開并返回到應(yīng)用垒在,現(xiàn)有UI將立即可見,因?yàn)?repository 從內(nèi)存中檢索緩存的數(shù)據(jù)扔亥。但是,如果用戶離開應(yīng)用程序并且數(shù)小時(shí)后回來谈为,或Android 系統(tǒng)殺死該進(jìn)程后旅挤,會(huì)發(fā)生什么?
在目前的實(shí)現(xiàn)中伞鲫,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)粘茄。這不僅是一個(gè)糟糕的用戶體驗(yàn),而且會(huì)浪費(fèi)資源秕脓,因?yàn)樗鼤?huì)使用移動(dòng)數(shù)據(jù)重新獲取相同的數(shù)據(jù)柒瓣。你可以簡(jiǎn)單地通過緩存Web請(qǐng)求來解決這個(gè)問題,但是這會(huì)產(chǎn)生新的問題吠架。如果相同的用戶數(shù)據(jù)從另一種類型的請(qǐng)求中顯示出來(例如芙贫,獲取朋友列表),會(huì)發(fā)生什么情況傍药?那么你的應(yīng)用程序可能會(huì)顯示不一致的數(shù)據(jù)磺平,這是一個(gè)混亂的用戶體驗(yàn)。例如拐辽,由于好友列表請(qǐng)求和用戶請(qǐng)求可以在不同的時(shí)間執(zhí)行拣挪,所以相同用戶的數(shù)據(jù)可能會(huì)以不同的方式顯示。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)俱诸。
處理這個(gè)問題的正確方法是使用 持久化模型菠劝。這就是 Room 持久化庫(kù)可以拯救的地方。
Room 是一個(gè)對(duì)象映射庫(kù)睁搭,使用最小的模板代碼來提供本地?cái)?shù)據(jù)持久化赶诊。在編譯時(shí),它會(huì)根據(jù) Schema 驗(yàn)證每個(gè)查詢园骆,因此甫何,有問題的SQL查詢會(huì)導(dǎo)致編譯時(shí)出錯(cuò),而不是運(yùn)行時(shí)失敗遇伞。Room 抽象了處理原始SQL表和查詢的一些底層實(shí)現(xiàn)細(xì)節(jié)辙喂。它還允許觀察對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和 join 查詢)的更改,通過 LiveData對(duì)象 公開這些更改 。另外巍耗,它明確定義了解決常見問題的線程約束秋麸,例如在主線程上的訪問存儲(chǔ)。
注意:如果你的應(yīng)用程序已經(jīng)使用另一個(gè)持久化解決方案(如SQLite對(duì)象關(guān)系映射(ORM))炬太,則不需要使用 Room 替換現(xiàn)有的解決方案灸蟆。但是,如果你正在編寫新的應(yīng)用程序或重構(gòu)現(xiàn)有的應(yīng)用程序亲族,我們建議使用 Room 來保存應(yīng)用程序的數(shù)據(jù)炒考。這樣,你可以利用庫(kù)的抽象和查詢 驗(yàn)證功能霎迫。
要使用 Room斋枢,我們需要定義我們的本地 Schema。首先知给,使用 @Entity 注解 User 類 以將其標(biāo)記為數(shù)據(jù)庫(kù)中的表瓤帚。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,為我們的 app 創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)類繼承于 RoomDatabase:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意 MyDatabase 是抽象的涩赢。Room 自動(dòng)提供一個(gè)它的實(shí)現(xiàn)戈次。有關(guān)詳細(xì)信息,請(qǐng)查看 Android架構(gòu)組件- Room數(shù)據(jù)庫(kù)的使用
現(xiàn)在我們需要一種將用戶數(shù)據(jù)插入數(shù)據(jù)庫(kù)的方法筒扒。為此怯邪,我們將創(chuàng)建一個(gè)數(shù)據(jù)訪問對(duì)象(DAO: data access object)。
@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 (Data Access Object)擎颖。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請(qǐng)注意,該 load 方法返回一個(gè) LiveData观游。Room 知道數(shù)據(jù)庫(kù)何時(shí)被修改搂捧,當(dāng)數(shù)據(jù)改變時(shí)它會(huì)自動(dòng)通知所有活躍的的察者。因?yàn)樗褂玫氖?LiveData懂缕,所以這將是有效的允跑,因?yàn)橹挥兄辽儆幸粋€(gè)活動(dòng)的觀察者才會(huì)更新數(shù)據(jù)。
注意:Room 根據(jù) table 的修改來檢查失效搪柑,這意味著它可能發(fā)送誤報(bào)的通知聋丝。
現(xiàn)在我們可以修改 UserRepository 來包含 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);
// return a LiveData directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// running in a background thread
// check if user was fetched recently
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// refresh the data
Response response = webservice.getUser(userId).execute();
// TODO check for error etc.
// Update the database.The LiveData will automatically refresh so
// we don't need to do anything else here besides updating the database
userDao.save(response.body());
}
});
}
}
請(qǐng)注意工碾,盡管我們改變了 來自于 UserRepository 的數(shù)據(jù)弱睦,我們并不需要改變我們 UserProfileViewModel
或 UserProfileFragment
。這是抽象提供的靈活性渊额。這對(duì)于測(cè)試來說有好處的况木,因?yàn)槟憧梢栽跍y(cè)試你的UserProfileViewModel
的時(shí)候提供一個(gè)假的 UserRepository
垒拢。
現(xiàn)在我們的代碼是完整了。如果用戶以后回到相同的用戶界面火惊,他們會(huì)立即看到用戶信息求类,因?yàn)槲覀兂志没恕M瑫r(shí)屹耐,如果數(shù)據(jù)過期了尸疆,我們的倉(cāng)庫(kù)將在后臺(tái)更新數(shù)據(jù)。當(dāng)然惶岭,根據(jù)您的使用情況寿弱,如果數(shù)據(jù)太舊,您可能不希望顯示持久化數(shù)據(jù)按灶。
在一些使用情況下症革,如 下拉刷新,UI 顯示用戶是否正在進(jìn)行網(wǎng)絡(luò)操作是非常重要的兆衅。將UI 操作與實(shí)際數(shù)據(jù)分開是一個(gè)很好的做法,因?yàn)樗赡芤蚋鞣N原因而導(dǎo)致更新(例如嗜浮,如果我們獲取朋友列表羡亩,同一用戶可能會(huì)再次觸發(fā) LiveData<User>
更新)。站在UI 的角度危融,事實(shí)上畏铆,當(dāng)有一個(gè)請(qǐng)求執(zhí)行的時(shí)候,另一個(gè)數(shù)據(jù)點(diǎn)吉殃,類似于任何其他的數(shù)據(jù) (比如 User
對(duì)象)辞居。
這個(gè)用例有兩種常見的解決方案:
更改
getUser
為返回包含網(wǎng)絡(luò)操作狀態(tài)的 LiveData 。附錄中提供了一個(gè)示例實(shí)現(xiàn):公開網(wǎng)絡(luò)狀態(tài)部分蛋勺。在 repository 類中提供另一個(gè)可以返回用戶刷新狀態(tài)的公共函數(shù)瓦灶。如果只想響應(yīng)顯式的用戶操作(如下拉刷新)來顯示網(wǎng)絡(luò)狀態(tài),則此選項(xiàng)更好抱完。
單一的真相來源:
不同的 REST API 端點(diǎn)通常返回相同的數(shù)據(jù)贼陶。例如,如果我們的服務(wù)器端擁有另一個(gè)返回 朋友列表的端點(diǎn)巧娱,則同一個(gè)用戶對(duì)象可能來自兩個(gè)不同的API 端點(diǎn)碉怔,也許粒度不同。如果 UserRepository
從 Webservice
請(qǐng)求返回原本的響應(yīng)禁添,我們的UI可能會(huì)顯示不一致的數(shù)據(jù)撮胧,因?yàn)樵谶@些請(qǐng)求過程中數(shù)據(jù)可能已經(jīng)在服務(wù)器端發(fā)生了改變。這就是為什么在 UserRepository
實(shí)現(xiàn)中老翘,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)中芹啥。然后锻离,對(duì)數(shù)據(jù)庫(kù)的更改將觸發(fā)回調(diào)給 活躍狀態(tài)的 LiveData
對(duì)象。
在這個(gè)模型中叁征,數(shù)據(jù)庫(kù)充當(dāng)了 單一的真相來源纳账,應(yīng)用程序的其他部分通過 Repository 訪問它。無論你是否使用磁盤緩存捺疼,我們都建議將你的 Repository 指定為應(yīng)用程序其余部分唯一的真相來源疏虫。
測(cè)試
我們已經(jīng)提到分離的好處之一就是可測(cè)試性,讓我們看看如何測(cè)試每個(gè)代碼模塊啤呼。
用戶界面和交互:你唯一需要花費(fèi)時(shí)間的是 Android UI Instrumentation 卧秘。測(cè)試UI 代碼的最好方法是創(chuàng)建一個(gè) Espresso測(cè)試。您可以創(chuàng)建 Fragment 并為其提供一個(gè)模擬的ViewModel官扣。由于該 Fragment 只與 ViewModel 聯(lián)系翅敌,所以偽造它足以完全測(cè)試這個(gè)UI。
ViewModel:ViewModel 可以使用 JUnit 來測(cè)試 惕蹄。你只需要模擬 UserRepository 來測(cè)試它蚯涮。
UserRepository:你同樣也可以使用 JUnit 來測(cè)試 UserRepository。你需要模擬 Webservice 和 DAO卖陵。你可以測(cè)試它是否做出了正確的Web服務(wù)調(diào)用遭顶,并將結(jié)果保存到數(shù)據(jù)庫(kù)中,如果數(shù)據(jù)已緩存且最新泪蔫,則不會(huì)發(fā)出任何不必要的請(qǐng)求棒旗。因?yàn)?Webservice 和 UserDao 都是接口,你可以模擬它們撩荣,或者為更復(fù)雜的測(cè)試用例創(chuàng)建偽造的實(shí)現(xiàn)…
UserDao:測(cè)試 DAO 類的推薦方法是使用 instrumentation 測(cè)試铣揉。由于這些 instrumentation 測(cè)試不需要任何用戶界面,他們將會(huì)運(yùn)行得很快餐曹。對(duì)于每個(gè)測(cè)試逛拱,您可以創(chuàng)建一個(gè)處于內(nèi)存中的數(shù)據(jù)庫(kù),以確保測(cè)試沒有任何副作用(如更改磁盤上的數(shù)據(jù)庫(kù)文件)台猴。
Room 也允許指定數(shù)據(jù)庫(kù)的實(shí)現(xiàn)橘券,所以你可以通過提供 JUnit 來測(cè)試 SupportSQLiteOpenHelper 的實(shí)現(xiàn)。通常不建議使用這種方法卿吐,因?yàn)樵O(shè)備上運(yùn)行的SQLite版本可能與主機(jī)上的SQLite版本不同旁舰。Webservice:使測(cè)試獨(dú)立于外界是很重要的,所以你的 Webservice 測(cè)試也應(yīng)該避免對(duì)后端進(jìn)行網(wǎng)絡(luò)調(diào)用嗡官。有很多庫(kù)可以幫助你箭窜,例如, MockWebServer 是一個(gè)強(qiáng)大的庫(kù)衍腥,可以幫助你為測(cè)試創(chuàng)建一個(gè)偽造的本地服務(wù)器磺樱。
-
Testing Artifacts 架構(gòu)組件提供了一個(gè)Maven artifact 來控制其后臺(tái)線程纳猫。在android.arch.core:core-testing artifact 內(nèi)部 ,有2個(gè) JUnit 規(guī)則:
- InstantTaskExecutorRule:此規(guī)則可用于強(qiáng)制架構(gòu)組件立即在調(diào)用線程上執(zhí)行任何后臺(tái)操作竹捉。
- CountingTaskExecutorRule:此規(guī)則可用于檢測(cè)測(cè)試芜辕,以等待架構(gòu)組件的后臺(tái)操作或?qū)⑵渥鳛殚e置資源連接到 Espresso。
最終的架構(gòu)
下圖顯示了我們推薦的架構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>
指導(dǎo)原則
編程是一個(gè)創(chuàng)造性的領(lǐng)域块差,開發(fā)Android應(yīng)用也不例外侵续。解決問題的方法有很多種,可以在多個(gè)Activity 或 Fragment 之間傳遞數(shù)據(jù)憨闰,檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進(jìn)行離線模式状蜗,也可以使用其他常見應(yīng)用程序遇到的情況。
雖然以下建議不是強(qiáng)制性的鹉动,但是根據(jù)我們的經(jīng)驗(yàn)轧坎,從長(zhǎng)遠(yuǎn)來看遵循這些建議將使您的代碼更加健壯,變得可測(cè)試和可維護(hù)泽示。
你在 AndroidManifest 中定義的入口點(diǎn)(activities, services, broadcast receivers, 等等)不是數(shù)據(jù)的來源缸血。相反,他們只應(yīng)該協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集械筛。由于每個(gè) app組件的 存活相當(dāng)短捎泻,這取決于用戶與設(shè)備的交互以及當(dāng)前運(yùn)行時(shí)的狀況,因此你不希望這些入口點(diǎn)中的任何一個(gè)成為數(shù)據(jù)源变姨。
在應(yīng)用程序的各個(gè)模塊之間建立明確的職責(zé)界限時(shí)要毫不留情族扰。例如厌丑,不要將從網(wǎng)絡(luò)中加載數(shù)據(jù)的代碼分散到多個(gè)類或包中定欧。同樣,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個(gè)類中怒竿。
盡可能少地暴露每個(gè)模塊砍鸠。不要試圖創(chuàng)建“只有那一個(gè)”的快捷方式,從一個(gè)模塊公開其內(nèi)部實(shí)現(xiàn)細(xì)節(jié)耕驰。你可能會(huì)在短期內(nèi)獲得一些時(shí)間爷辱,但隨著代碼庫(kù)的不斷發(fā)展,你將會(huì)花費(fèi)更多時(shí)間付出技術(shù)代價(jià)朦肘。
在定義模塊之間的交互時(shí)饭弓,請(qǐng)考慮如何使每個(gè)模塊獨(dú)立地進(jìn)行測(cè)試。例如媒抠,如果有一個(gè)定義良好的 API 從網(wǎng)絡(luò)中獲取數(shù)據(jù)弟断,測(cè)試將數(shù)據(jù)保存在本地?cái)?shù)據(jù)庫(kù)中的模塊會(huì)變得更容易。相反趴生,如果將這兩個(gè)模塊的邏輯混合在一起阀趴,或者在整個(gè)代碼庫(kù)中分散網(wǎng)絡(luò)請(qǐng)求代碼昏翰,那么要測(cè)試就會(huì)更加困難。
你的APP的核心是讓它從其他APP中脫穎而出刘急。不要花費(fèi)時(shí)間重新造輪子棚菊,或者一次又一次地寫出相同的樣板代碼。相反叔汁,將精力集中在可以讓你的應(yīng)用獨(dú)特的東西上统求,讓Android 架構(gòu) 和其他推薦的庫(kù)處理重復(fù)的樣板代碼。
持久化盡可能多的相關(guān)和最新的數(shù)據(jù)攻柠,以便當(dāng)設(shè)備處于離線模式時(shí)你的APP依然可用球订。雖然你可能喜歡恒定的高速連接,但你的用戶可能并不會(huì)瑰钮。
你的 repository 應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一的事實(shí)來源冒滩。無論你的應(yīng)用程序何時(shí)需要訪問這些數(shù)據(jù),都應(yīng)始終從單一的事實(shí)源頭發(fā)起浪谴。有關(guān)更多信息开睡,請(qǐng)查看 單一的真相來源
附錄:暴露網(wǎng)絡(luò)狀態(tài)
在上面推薦的App架構(gòu)部分,我們故意省略了網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)苟耻,以保持示例的簡(jiǎn)單篇恒。在本節(jié)中,我們演示了如何使用 Resource
類來暴露網(wǎng)絡(luò)狀態(tài)以及封裝數(shù)據(jù)及其狀態(tài)凶杖。
以下是一個(gè)實(shí)現(xiàn)的例子:
//a generic class that describes a data with a status
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)閺木W(wǎng)絡(luò)加載數(shù)據(jù)胁艰,而從磁盤顯示數(shù)據(jù)是一個(gè)常見的用例,我們將創(chuàng)建一個(gè)輔助類 NetworkBoundResource
智蝠,它可以在多個(gè)地方重復(fù)使用腾么。以下是 NetworkBoundResource
的決策樹:
它首先通過對(duì)資源的數(shù)據(jù)庫(kù)進(jìn)行觀察。當(dāng)?shù)谝淮螐臄?shù)據(jù)庫(kù)加載條目時(shí)杈湾,NetworkBoundResource0 會(huì)檢查結(jié)果是否足夠好以便被分發(fā)解虱,或者它應(yīng)該從網(wǎng)絡(luò)中獲取。請(qǐng)注意漆撞,這兩種情況可能同時(shí)發(fā)生殴泰,因?yàn)槟憧赡芟M趶木W(wǎng)絡(luò)獲取數(shù)據(jù)時(shí)顯示緩存的數(shù)據(jù)。
如果網(wǎng)絡(luò)調(diào)用成功完成浮驳,則將響應(yīng)保存到數(shù)據(jù)庫(kù)中并重新初始化流悍汛。如果網(wǎng)絡(luò)請(qǐng)求失敗,我們直接發(fā)送失敗至会。
注意:在將新數(shù)據(jù)保存到磁盤之后离咐,我們會(huì)重新初始化數(shù)據(jù)庫(kù)中的數(shù)據(jù)流,但通常我們不需要這樣做奋献,因?yàn)閿?shù)據(jù)庫(kù)將會(huì)發(fā)送更改健霹。另一方面旺上,依靠數(shù)據(jù)庫(kù)來發(fā)送更改,將產(chǎn)生依賴副作用糖埋,因?yàn)槿绻麛?shù)據(jù)沒有變化宣吱,數(shù)據(jù)庫(kù)可以避免發(fā)送變化,那么它可能會(huì)中斷瞳别。我們也不希望發(fā)送從網(wǎng)絡(luò)返回的結(jié)果征候,因?yàn)檫@將違背單一的真相來源(也許數(shù)據(jù)庫(kù)中會(huì)有觸發(fā)機(jī)制可以改變保存的值)。我們也不想在沒有新數(shù)據(jù)的情況下發(fā)送
SUCCESS
祟敛,因?yàn)樗鼤?huì)向客戶發(fā)送錯(cuò)誤的信息疤坝。
下面是 公開API 是為 NetworkBoundResource 的子類提供的 :
// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
// Called to save the result of the API response into the database
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// Called with the data in the database to decide whether it should be
// fetched from the network.
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached data from the database
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.
@MainThread
protected void onFetchFailed() {
}
// returns a LiveData that represents the resource, implemented
// in the base class.
public final LiveData<Resource<ResultType>> getAsLiveData();
}
請(qǐng)注意,上面的類定義了兩個(gè)類型參數(shù)(ResultType
馆铁, RequestType
)跑揉,因?yàn)閺?API 返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配。
另請(qǐng)注意埠巨,上面的 ApiResponse
代碼用于網(wǎng)絡(luò)請(qǐng)求历谍。 ApiResponse
是一個(gè)簡(jiǎn)單的Retrofit2.Call
類包裝,將其響應(yīng)轉(zhuǎn)換為 LiveData
辣垒。
以下是該 NetworkBoundResource
類的其余部分:
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();
// we re-attach dbSource as a new source,
// it will dispatch its latest value quickly
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) {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
現(xiàn)在望侈,我們可以使用 NetworkBoundResource
將在 repository 中綁定 User
實(shí)現(xiàn) 寫入我們的磁盤和網(wǎng)絡(luò)。
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();
}
}
本文轉(zhuǎn)自張華洋的博客