App體系結(jié)構(gòu)指南
本指南適用于那些過(guò)去構(gòu)建應(yīng)用程序的基礎(chǔ)知識(shí)的開(kāi)發(fā)人員,現(xiàn)在想知道構(gòu)建強(qiáng)大的生產(chǎn)質(zhì)量應(yīng)用程序的最佳實(shí)踐和建議的體系結(jié)構(gòu)。
注意:本指南假定讀者熟悉Android框架。如果您不熟悉應(yīng)用程序開(kāi)發(fā),請(qǐng)查看 入門(mén) 培訓(xùn)系列,其中包含本指南的必備主題
應(yīng)用開(kāi)發(fā)者面臨的常見(jiàn)問(wèn)題
與傳統(tǒng)的桌面應(yīng)用程序不同,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多,在大多數(shù)情況下,它們只有一個(gè)啟動(dòng)快捷方式的入口點(diǎn),并且可以作為一個(gè)整體進(jìn)程運(yùn)行。一個(gè)典型的Android應(yīng)用程序是由多個(gè)應(yīng)用程序組件構(gòu)成的,包括活動(dòng),片段,服務(wù),內(nèi)容提供者和廣播接收器。
大多數(shù)這些應(yīng)用程序組件都是在Android操作系統(tǒng)使用的應(yīng)用程序清單中聲明的,以決定如何將您的應(yīng)用程序與其設(shè)備的整體用戶(hù)體驗(yàn)相集成。雖然如前所述,桌面應(yīng)用程序傳統(tǒng)上是以整體的方式運(yùn)行的,但正確編寫(xiě)的Android應(yīng)用程序需要更加靈活,因?yàn)橛脩?hù)可以通過(guò)設(shè)備上的不同應(yīng)用程序進(jìn)行編程,不斷切換流程和任務(wù)。
例如,請(qǐng)考慮在您最喜愛(ài)的社交網(wǎng)絡(luò)應(yīng)用程序中分享照片時(shí)會(huì)發(fā)生什么情況。該應(yīng)用程序觸發(fā)Android操作系統(tǒng)啟動(dòng)相機(jī)應(yīng)用程序來(lái)處理請(qǐng)求的相機(jī)意圖。此時(shí),用戶(hù)離開(kāi)了社交網(wǎng)絡(luò)應(yīng)用,但他們的體驗(yàn)是無(wú)縫的。相機(jī)應(yīng)用程序又可能觸發(fā)其他意圖,例如啟動(dòng)文件選擇器,該文件選擇器可以啟動(dòng)另一個(gè)應(yīng)用程序。最終用戶(hù)回到社交網(wǎng)絡(luò)應(yīng)用程序并分享照片。此外,用戶(hù)在這個(gè)過(guò)程的任何時(shí)候都可能被電話(huà)打斷,并在打完電話(huà)后回來(lái)分享照片。
在Android中,這種應(yīng)用程序跳轉(zhuǎn)行為很常見(jiàn),所以您的應(yīng)用程序必須正確處理這些流程。請(qǐng)記住,移動(dòng)設(shè)備是資源受限,所以在任何時(shí)候,操作系統(tǒng)可能需要?dú)⑺酪恍?yīng)用程序,以騰出空間給新的。
所有這一切的關(guān)鍵是,您的應(yīng)用程序組件可以單獨(dú)和無(wú)序地啟動(dòng),并可以在任何時(shí)候由用戶(hù)或系統(tǒng)銷(xiāo)毀。由于應(yīng)用程序組件是短暫的,它們的生命周期(創(chuàng)建和銷(xiāo)毀時(shí))不在您的控制之下,因此您不應(yīng)該在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用程序數(shù)據(jù)或狀態(tài),并且應(yīng)用程序組件不應(yīng)相互依賴(lài)。
共同的建筑原則
如果您不能使用應(yīng)用程序組件來(lái)存儲(chǔ)應(yīng)用程序數(shù)據(jù)和狀態(tài),應(yīng)該如何構(gòu)建應(yīng)用程序?
你應(yīng)該關(guān)注的最重要的事情是在你的應(yīng)用程序中分離關(guān)注點(diǎn)。將所有的代碼寫(xiě)入一個(gè)Activity或一個(gè)常見(jiàn)的錯(cuò)誤Fragment。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類(lèi)中。盡可能保持精簡(jiǎn)可以避免許多生命周期相關(guān)的問(wèn)題。不要忘記,你不擁有這些類(lèi),它們只是體現(xiàn)操作系統(tǒng)和你的應(yīng)用程序之間的契約的膠水類(lèi)。Android操作系統(tǒng)可能會(huì)隨時(shí)根據(jù)用戶(hù)交互或其他因素(如低內(nèi)存)來(lái)銷(xiāo)毀它們。最好最大限度地減少對(duì)他們的依賴(lài),以提供可靠的用戶(hù)體驗(yàn)。
第二個(gè)重要的原則是你應(yīng)該從一個(gè)模型驅(qū)動(dòng)你的UI,最好是一個(gè)持久模型。持久性是理想的,原因有兩個(gè):如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源,則您的用戶(hù)不會(huì)丟失數(shù)據(jù),即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,您的應(yīng)用程序也將繼續(xù)工作。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件。它們獨(dú)立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問(wèn)題是隔離的。保持簡(jiǎn)單的UI代碼和免費(fèi)的應(yīng)用程序邏輯,使管理更容易。將您的應(yīng)用程序放在模型類(lèi)上,具有明確的數(shù)據(jù)管理責(zé)任將使他們可測(cè)試,并使您的應(yīng)用程序保持一致。
推薦的應(yīng)用架構(gòu)
在本節(jié)中,我們將演示如何通過(guò)使用用例來(lái)構(gòu)建使用體系結(jié)構(gòu)組件的應(yīng)用程序。
注意:不可能有一種編寫(xiě)應(yīng)用程序的方法,這對(duì)每種情況都是最好的。這就是說(shuō),這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個(gè)很好的起點(diǎn)。如果您已經(jīng)有了編寫(xiě)Android應(yīng)用程序的好方法,則不需要更改。
想象一下,我們正在構(gòu)建一個(gè)顯示用戶(hù)配置文件的用戶(hù)界面。該用戶(hù)配置文件將使用REST API從我們自己的私人后端獲取。
構(gòu)建用戶(hù)界面
UI將由一個(gè)片段UserProfileFragment.java及其相應(yīng)的布局文件組成user_profile_layout.xml。
為了驅(qū)動(dòng)用戶(hù)界面,我們的數(shù)據(jù)模型需要保存兩個(gè)數(shù)據(jù)元素。
- 用戶(hù)ID:用戶(hù)的標(biāo)識(shí)符。最好使用片段參數(shù)將此信息傳遞到片段中。如果Android操作系統(tǒng)破壞您的進(jìn)程,這些信息將被保留,以便在您的應(yīng)用下次重新啟動(dòng)時(shí)可用。
- 用戶(hù)對(duì)象:保存用戶(hù)數(shù)據(jù)的POJO。
我們將創(chuàng)建一個(gè)UserProfileViewModel基于ViewModel的類(lèi)來(lái)保存這些信息。
甲視圖模型提供了一個(gè)特定的UI組件中的數(shù)據(jù),如一個(gè)片段或活性,和處理與數(shù)據(jù)處理的部分業(yè)務(wù),如主叫其他組件加載數(shù)據(jù)或轉(zhuǎn)發(fā)的用戶(hù)修改的通信。ViewModel不知道視圖,并且不受配置更改的影響,例如由于旋轉(zhuǎn)而重新創(chuàng)建活動(dòng)。
現(xiàn)在我們有3個(gè)文件。
- user_profile.xml:屏幕的UI定義。
- UserProfileViewModel.java:為UI準(zhǔn)備數(shù)據(jù)的類(lèi)。
- UserProfileFragment.java:在ViewModel中顯示數(shù)據(jù)并對(duì)用戶(hù)交互作出反應(yīng)的UI控制器。
下面是我們的開(kāi)始的實(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 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)在,我們有這三個(gè)代碼模塊,我們?nèi)绾芜B接它們?畢竟,當(dāng)ViewModel的用戶(hù)字段被設(shè)置,我們需要一種方式來(lái)通知用戶(hù)界面。這是LiveData類(lèi)的地方。
LiveData是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData 對(duì)象的更改,而不會(huì)在它們之間創(chuàng)建明確的和嚴(yán)格的依賴(lài)關(guān)系路徑。LiveData還尊重您的應(yīng)用程序組件(活動(dòng),片段,服務(wù))的生命周期狀態(tài),并做正確的事情來(lái)防止對(duì)象泄漏,使您的應(yīng)用程序不會(huì)消耗更多的內(nèi)存。
注意:如果您已經(jīng)在使用類(lèi)似RxJava或 Agera的庫(kù) ,則可以繼續(xù)使用它們而不是LiveData。但是,當(dāng)您使用它們或其他方法時(shí),請(qǐng)確保正確處理生命周期,以便在相關(guān)的LifecycleOwner停止時(shí)停止數(shù)據(jù)流,并在銷(xiāo)毀LifecycleOwner時(shí)銷(xiāo)毀數(shù)據(jù)流。您還可以添加 android.arch.lifecycle:reactivestreams工件以將LiveData與另一個(gè)反應(yīng)流庫(kù)(例如RxJava2)一起使用。
現(xiàn)在我們用UserProfileViewModel 替換User字段,以便在數(shù)據(jù)更新時(shí)通知fragment。重要的 是,它是生命周期感知,當(dāng)我們長(zhǎng)時(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
});
}
每次更新用戶(hù)數(shù)據(jù)時(shí), 都會(huì)調(diào)用onChanged回調(diào),并刷新UI。
如果您熟悉使用可觀察回調(diào)的其他庫(kù),您可能已經(jīng)意識(shí)到,我們不必重寫(xiě)片段的onStop()方法來(lái)停止觀察數(shù)據(jù)。這對(duì)于LiveData來(lái)說(shuō)是不必要的,因?yàn)樗巧芷诟兄模@意味著它不會(huì)調(diào)用回調(diào),除非片段處于活動(dòng)狀態(tài)(已接收onStart()但未接收onStop())。當(dāng)數(shù)據(jù)片段收到時(shí),LiveData也會(huì)自動(dòng)移除觀察者onDestroy()。
我們也沒(méi)有做任何特殊的處理配置變化(例如,用戶(hù)旋轉(zhuǎn)屏幕)。當(dāng)配置改變時(shí),ViewModel會(huì)自動(dòng)恢復(fù),所以一旦新的片段生效,它將接收到相同的ViewModel實(shí)例,并且回調(diào)將被當(dāng)前數(shù)據(jù)立即調(diào)用。這就是ViewModel不能直接引用Views的原因。他們可以超越View的生命周期。請(qǐng)參閱 ViewModel的生命周期。
正在提取數(shù)據(jù)
現(xiàn)在我們已經(jīng)將ViewModel連接到了片段,但是ViewModel如何獲取用戶(hù)數(shù)據(jù)呢?在這個(gè)例子中,我們假設(shè)我們的后端提供了一個(gè)REST API。我們將使用 Retrofit庫(kù)來(lái)訪(fǎng)問(wèn)我們的后端,盡管您可以自由使用不同的庫(kù)來(lái)達(dá)到同樣的目的。
以下是我們Webservice與后端進(jìn)行通信的改造:
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;
}
}
即使存儲(chǔ)庫(kù)模塊看起來(lái)沒(méi)有必要,它也是一個(gè)重要的目的。它從應(yīng)用程序的其余部分提取數(shù)據(jù)源。現(xiàn)在我們的ViewModel不知道數(shù)據(jù)是由Webservice哪個(gè)取得的,這意味著我們可以根據(jù)需要將它交換為其他的實(shí)現(xiàn)。
注意:為了簡(jiǎn)單起見(jiàn),我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況。有關(guān)公開(kāi)錯(cuò)誤和加載狀態(tài)的替代實(shí)現(xiàn),請(qǐng)參閱 附錄:公開(kāi)網(wǎng)絡(luò)狀態(tài)。
管理組件之間的依賴(lài)關(guān)系:
UserRepository上面的類(lèi)需要一個(gè)實(shí)例Webservice來(lái)完成它的工作。它可以簡(jiǎn)單地創(chuàng)建它,但要做到這一點(diǎn),它也需要知道Webservice類(lèi)的依賴(lài)關(guān)系來(lái)構(gòu)造它。這會(huì)使代碼復(fù)雜化和復(fù)制(例如,每個(gè)需要Webservice實(shí)例的類(lèi) 都需要知道如何用它的依賴(lài)來(lái)構(gòu)造它)。另外,UserRepository可能不是唯一需要的類(lèi)Webservice。如果每個(gè)班級(jí)創(chuàng)建一個(gè)新的WebService,這將是非常重的資源。
有兩種模式可以用來(lái)解決這個(gè)問(wèn)題:
- 依賴(lài)注入:依賴(lài)注入允許類(lèi)在不構(gòu)造它們的情況下定義它們的依賴(lài)關(guān)系。在運(yùn)行時(shí),另一個(gè)類(lèi)負(fù)責(zé)提供這些依賴(lài)關(guān)系。我們推薦Google的Dagger 2庫(kù)在Android應(yīng)用程序中實(shí)現(xiàn)依賴(lài)注入。Dagger 2通過(guò)遍歷依賴(lài)關(guān)系樹(shù)來(lái)自動(dòng)構(gòu)造對(duì)象,并為依賴(lài)關(guān)系提供編譯時(shí)間保證。
- 服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊(cè)表,類(lèi)可以獲得它們的依賴(lài)而不是構(gòu)建它們。實(shí)現(xiàn)起來(lái)比依賴(lài)注入(DI)更容易,所以如果你不熟悉DI,可以使用Service Locator。
在這個(gè)例子中,我們將使用Dagger 2來(lái)管理依賴(lài)關(guān)系。
連接ViewModel和存儲(chǔ)庫(kù)
現(xiàn)在我們修改我們UserProfileViewModel的存儲(chǔ)庫(kù)。
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ù)
上面的存儲(chǔ)庫(kù)實(shí)現(xiàn)對(duì)抽象調(diào)用Web服務(wù)是有好處的,但是因?yàn)樗灰蕾?lài)于一個(gè)數(shù)據(jù)源,所以它不是很實(shí)用。
UserRepository上面的實(shí)現(xiàn)的問(wèn)題是,在獲取數(shù)據(jù)之后,它不保存在任何地方。如果用戶(hù)離開(kāi) UserProfileFragment并返回,應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)。這是不好的,原因有兩個(gè):浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬并強(qiáng)制用戶(hù)等待新的查詢(xún)完成。為了解決這個(gè)問(wèn)題,我們將添加一個(gè)新的數(shù)據(jù)源,我們UserRepository將緩存User內(nèi)存中的對(duì)象。
@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)中,如果用戶(hù)旋轉(zhuǎn)屏幕或離開(kāi)并返回到應(yīng)用程序,則現(xiàn)有UI將立即可見(jiàn),因?yàn)榇鎯?chǔ)庫(kù)從內(nèi)存中高速緩存中檢索數(shù)據(jù)。但是,如果用戶(hù)離開(kāi)應(yīng)用程序,并在Android操作系統(tǒng)殺死該進(jìn)程后數(shù)小時(shí)后回來(lái),會(huì)發(fā)生什么?
在目前的實(shí)施中,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)。這不僅是一個(gè)糟糕的用戶(hù)體驗(yàn),而且會(huì)浪費(fèi),因?yàn)樗鼤?huì)使用移動(dòng)數(shù)據(jù)重新獲取相同的數(shù)據(jù)。您可以簡(jiǎn)單地通過(guò)緩存Web請(qǐng)求來(lái)解決這個(gè)問(wèn)題,但是會(huì)產(chǎn)生新的問(wèn)題。如果相同的用戶(hù)數(shù)據(jù)顯示來(lái)自另一種類(lèi)型的請(qǐng)求(例如,獲取朋友列表),會(huì)發(fā)生什么情況?那么你的應(yīng)用程序可能會(huì)顯示不一致的數(shù)據(jù),這是一個(gè)混亂的用戶(hù)體驗(yàn)充其量。例如,由于好友列表請(qǐng)求和用戶(hù)請(qǐng)求可以在不同的時(shí)間執(zhí)行,所以相同用戶(hù)的數(shù)據(jù)可能會(huì)以不同的方式顯示。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)。
處理這個(gè)問(wèn)題的正確方法是使用持久模型。這是 Room 持久性庫(kù)來(lái)救援的地方。
Room 是一個(gè)對(duì)象映射庫(kù),提供本地?cái)?shù)據(jù)持久性和最小的樣板代碼。在編譯時(shí),它會(huì)根據(jù)模式驗(yàn)證每個(gè)查詢(xún),以便斷開(kāi)的SQL查詢(xún)導(dǎo)致編譯時(shí)錯(cuò)誤,而不是運(yùn)行時(shí)失敗。會(huì)議室抽象出一些使用原始SQL表和查詢(xún)的底層實(shí)現(xiàn)細(xì)節(jié)。它還允許觀察對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢(xún))的更改,通過(guò)LiveData對(duì)象公開(kāi)這些更改 。另外,它明確定義了解決常見(jiàn)問(wèn)題的線(xiàn)程約束,例如訪(fǎng)問(wèn)主線(xiàn)程上的存儲(chǔ)。
注意:如果您的應(yīng)用程序已經(jīng)使用另一個(gè)持久性解決方案(如SQLite對(duì)象關(guān)系映射(ORM)),則不需要使用Room替換現(xiàn)有的解決方案。但是,如果您正在編寫(xiě)新的應(yīng)用程序或重構(gòu)現(xiàn)有的應(yīng)用程序,我們建議使用Room來(lái)保存應(yīng)用程序的數(shù)據(jù)。這樣,您可以利用庫(kù)的抽象和查詢(xún)驗(yàn)證功能。
要使用Room,我們需要定義我們的本地模式。首先,注釋User 該類(lèi)以@Entity 將其標(biāo)記為數(shù)據(jù)庫(kù)中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,通過(guò)擴(kuò)展 RoomDatabase 您的應(yīng)用程序來(lái)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)類(lèi) :
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意這MyDatabase是抽象的。房間自動(dòng)提供一個(gè)實(shí)施。有關(guān)詳細(xì)信息,請(qǐng)參見(jiàn)房間文檔
現(xiàn)在我們需要一種將用戶(hù)數(shù)據(jù)插入數(shù)據(jù)庫(kù)的方法。為此,我們將創(chuàng)建一個(gè)數(shù)據(jù)訪(fǎng)問(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ù)類(lèi)中引用DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請(qǐng)注意,該load方法返回一個(gè)LiveData<User>。Room知道數(shù)據(jù)庫(kù)何時(shí)被修改,當(dāng)數(shù)據(jù)改變時(shí)它會(huì)自動(dòng)通知所有活動(dòng)的觀察者。因?yàn)樗褂玫氖荓iveData,所以這將是有效的,因?yàn)樗粫?huì)在至少有一個(gè)活動(dòng)觀察者的情況下更新數(shù)據(jù)。
注意:Room根據(jù)表格修改檢查失效,這意味著它可能發(fā)送誤報(bào)通知。
現(xiàn)在我們可以修改我們UserRepository來(lái)合并房間數(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)注意,盡管我們改變了數(shù)據(jù)來(lái)自于 UserRepository,我們并不需要改變我們UserProfileViewModel或 UserProfileFragment。這是抽象提供的靈活性。這對(duì)于測(cè)試來(lái)說(shuō)也很棒,因?yàn)槟憧梢訳serRepository 在測(cè)試你的時(shí)候提供一個(gè)假的UserProfileViewModel。
現(xiàn)在我們的代碼是完整的。如果用戶(hù)以后回到相同的用戶(hù)界面,他們會(huì)立即看到用戶(hù)信息,因?yàn)槲覀兂志没?。同時(shí),如果數(shù)據(jù)陳舊,我們的倉(cāng)庫(kù)將在后臺(tái)更新數(shù)據(jù)。當(dāng)然,根據(jù)您的使用情況,如果數(shù)據(jù)太舊,您可能不希望顯示持久數(shù)據(jù)。
在一些使用情況下,如拉到刷新,UI顯示用戶(hù)是否正在進(jìn)行網(wǎng)絡(luò)操作是非常重要的。將UI操作與實(shí)際數(shù)據(jù)分開(kāi)是一種很好的做法,因?yàn)樗赡芤蚋鞣N原因而更新(例如,如果我們獲取朋友列表,可能會(huì)再次觸發(fā)同一用戶(hù)觸發(fā)LiveData<User>更新)。從用戶(hù)界面的角度來(lái)看,有一個(gè)請(qǐng)求在飛行的事實(shí)只是另一個(gè)數(shù)據(jù)點(diǎn),類(lèi)似于任何其他數(shù)據(jù)(如User對(duì)象)。
這個(gè)用例有兩種常見(jiàn)的解決方案:
- 更改getUser為返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。附錄中提供了一個(gè)示例實(shí)現(xiàn):公開(kāi)網(wǎng)絡(luò)狀態(tài)部分。
- 在存儲(chǔ)庫(kù)類(lèi)中提供另一個(gè)可以返回用戶(hù)刷新?tīng)顟B(tài)的公共函數(shù)。如果只想響應(yīng)顯式的用戶(hù)操作(如拉到刷新)來(lái)顯示網(wǎng)絡(luò)狀態(tài),則此選項(xiàng)更好。
單一的事實(shí)來(lái)源
不同的REST API端點(diǎn)通常返回相同的數(shù)據(jù)。例如,如果我們的后端擁有另一個(gè)返回朋友列表的端點(diǎn),則同一個(gè)用戶(hù)對(duì)象可能來(lái)自?xún)蓚€(gè)不同的API端點(diǎn),也許粒度不同。如果原樣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ù)的更改將觸發(fā)活動(dòng)LiveData對(duì)象上的回調(diào)。
在這個(gè)模型中,數(shù)據(jù)庫(kù)充當(dāng)真相的單一來(lái)源,應(yīng)用程序的其他部分通過(guò)存儲(chǔ)庫(kù)訪(fǎng)問(wèn)它。無(wú)論您使用磁盤(pán)緩存,我們都建議您的存儲(chǔ)庫(kù)將數(shù)據(jù)源指定為應(yīng)用程序其余部分的單一來(lái)源。
測(cè)試
我們已經(jīng)提到分離的好處之一就是可測(cè)試性。讓我們看看我們?nèi)绾螠y(cè)試每個(gè)代碼模塊。
用戶(hù)界面和交互:這將是您唯一需要進(jìn)行 Android UI Instrumentation測(cè)試的時(shí)間。測(cè)試UI代碼的最好方法是創(chuàng)建一個(gè) Espresso測(cè)試。您可以創(chuàng)建片段并為其提供一個(gè)模擬的ViewModel。由于該片段只與ViewModel交談,所以嘲笑它將足以完全測(cè)試這個(gè)UI。
ViewModel:可以使用JUnit測(cè)試來(lái)測(cè)試ViewModel 。你只需要嘲笑UserRepository測(cè)試它。
UserRepository:你也可以UserRepository用JUnit測(cè)試來(lái)測(cè)試。你需要嘲笑Webservice和DAO。您可以測(cè)試它是否做出正確的Web服務(wù)調(diào)用,將結(jié)果保存到數(shù)據(jù)庫(kù)中,如果數(shù)據(jù)已緩存且最新,則不會(huì)發(fā)出任何不必要的請(qǐng)求。由于這兩個(gè)Webservice和UserDao的界面,你可以嘲笑他們或創(chuàng)建更復(fù)雜的測(cè)試案例假冒實(shí)現(xiàn)..
UserDao:測(cè)試DAO類(lèi)的推薦方法是使用儀器測(cè)試。由于這些儀器測(cè)試不需要任何用戶(hù)界面,他們?nèi)匀粫?huì)運(yùn)行得很快。對(duì)于每個(gè)測(cè)試,您可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù),以確保測(cè)試沒(méi)有任何副作用(如更改磁盤(pán)上的數(shù)據(jù)庫(kù)文件)。
Room還允許指定數(shù)據(jù)庫(kù)的實(shí)現(xiàn),所以你可以通過(guò)提供JUnit實(shí)現(xiàn)來(lái)測(cè)試它 SupportSQLiteOpenHelper。通常不建議使用這種方法,因?yàn)樵O(shè)備上運(yùn)行的SQLite版本可能與主機(jī)上的SQLite版本不同。
Webservice:使測(cè)試獨(dú)立于外部是很重要的,所以即使你的Webservice測(cè)試也應(yīng)該避免對(duì)后端進(jìn)行網(wǎng)絡(luò)調(diào)用。有很多圖書(shū)館可以幫助你。例如, MockWebServer 是一個(gè)偉大的庫(kù),可以幫助您為測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)器。
-
測(cè)試工件體系結(jié)構(gòu)組件提供了一個(gè)Maven工件來(lái)控制其后臺(tái)線(xiàn)程。在android.arch.core:core-testing神器內(nèi)部 ,有2個(gè)JUnit規(guī)則:
InstantTaskExecutorRule:此規(guī)則可用于強(qiáng)制架構(gòu)組件立即在調(diào)用線(xiàn)程上執(zhí)行任何后臺(tái)操作。
CountingTaskExecutorRule:此規(guī)則可用于檢測(cè)測(cè)試,以等待體系結(jié)構(gòu)組件的后臺(tái)操作或?qū)⑵渥鳛殚e置資源連接到Espresso。
最終的體系結(jié)構(gòu)
下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>

指導(dǎo)原則
編程是一個(gè)創(chuàng)造性的領(lǐng)域,構(gòu)建Android應(yīng)用程序不是一個(gè)例外。解決問(wèn)題的方法有很多種,可以在多個(gè)活動(dòng)或片段之間傳遞數(shù)據(jù),檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進(jìn)行脫機(jī)模式,也可以使用許多其他常見(jiàn)應(yīng)用程序遇到的情況。
雖然以下建議不是強(qiáng)制性的,但是我們的經(jīng)驗(yàn)是,遵循這些建議將使您的代碼基礎(chǔ)更加健壯,可測(cè)試和可維護(hù)。
您在清單中定義的入口點(diǎn)(活動(dòng),服務(wù),廣播接收器等)不是數(shù)據(jù)的來(lái)源。相反,他們只應(yīng)該協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集。由于每個(gè)應(yīng)用程序組件的壽命相當(dāng)短,這取決于用戶(hù)與設(shè)備的交互以及運(yùn)行時(shí)的整體當(dāng)前運(yùn)行狀況,因此您不希望這些入口點(diǎn)中的任何一個(gè)成為數(shù)據(jù)源。
無(wú)情地在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確界定的責(zé)任。例如,不要將從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼跨代碼庫(kù)中的多個(gè)類(lèi)或包傳播。同樣,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個(gè)類(lèi)中。
盡可能少地從每個(gè)模塊公開(kāi)。不要試圖創(chuàng)建“只有那一個(gè)”的快捷方式,從一個(gè)模塊公開(kāi)內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。您可能在短期內(nèi)獲得一些時(shí)間,但隨著您的代碼庫(kù)的發(fā)展,您將多次支付技術(shù)債務(wù)。
在定義模塊之間的交互時(shí),請(qǐng)考慮如何使每個(gè)模塊獨(dú)立地進(jìn)行測(cè)試。例如,如果有一個(gè)定義良好的API從網(wǎng)絡(luò)中獲取數(shù)據(jù),將會(huì)更容易測(cè)試將數(shù)據(jù)保存在本地?cái)?shù)據(jù)庫(kù)中的模塊。相反,如果將這兩個(gè)模塊的邏輯混合在一起,或者在整個(gè)代碼庫(kù)中撒上網(wǎng)絡(luò)代碼,那么要測(cè)試就更加困難了。
你的應(yīng)用程序的核心是什么讓它從其他中脫穎而出。不要花費(fèi)時(shí)間重復(fù)發(fā)明輪子,或者一次又一次地寫(xiě)出相同的樣板代碼。相反,將精力集中在讓您的應(yīng)用獨(dú)特的東西上,讓Android Architecture組件和其他推薦的庫(kù)處理重復(fù)的樣板。
堅(jiān)持盡可能多的相關(guān)和新鮮的數(shù)據(jù),以便您的應(yīng)用程序在設(shè)備處于離線(xiàn)模式時(shí)可用。雖然您可以享受持續(xù)高速的連接,但用戶(hù)可能不會(huì)。
您的存儲(chǔ)庫(kù)應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一的事實(shí)來(lái)源。無(wú)論何時(shí)您的應(yīng)用程序需要訪(fǎng)問(wèn)這些數(shù)據(jù),都應(yīng)始終從單一的事實(shí)源頭開(kāi)始。有關(guān)更多信息,請(qǐng)參閱單一來(lái)源的真相。
附錄:揭露網(wǎng)絡(luò)狀態(tài)
在上面推薦的應(yīng)用程序體系結(jié)構(gòu)部分,我們故意省略網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài),以保持樣本簡(jiǎn)單。在本節(jié)中,我們演示一種使用Resource類(lèi)來(lái)公開(kāi)網(wǎng)絡(luò)狀態(tài)的方法來(lá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)樵趶拇疟P(pán)顯示數(shù)據(jù)時(shí)從網(wǎng)絡(luò)加載數(shù)據(jù)是一個(gè)常見(jiàn)的用例,我們將創(chuàng)建一個(gè)NetworkBoundResource可以在多個(gè)地方重復(fù)使用的幫助類(lèi)。以下是決策樹(shù) NetworkBoundResource:

它通過(guò)觀察資源的數(shù)據(jù)庫(kù)開(kāi)始。當(dāng)條目從數(shù)據(jù)庫(kù)中第一次被加載時(shí),NetworkBoundResource檢查結(jié)果是否足夠好以便被分派和/或它應(yīng)該從網(wǎng)絡(luò)中獲取。請(qǐng)注意,這兩種情況可能同時(shí)發(fā)生,因?yàn)槟赡芟M趶木W(wǎng)絡(luò)更新緩存數(shù)據(jù)時(shí)顯示緩存的數(shù)據(jù)。
如果網(wǎng)絡(luò)呼叫成功完成,則將響應(yīng)保存到數(shù)據(jù)庫(kù)中并重新初始化流。如果網(wǎng)絡(luò)請(qǐng)求失敗,我們直接發(fā)送失敗。
注意:在將新數(shù)據(jù)保存到磁盤(pán)之后,我們會(huì)重新初始化數(shù)據(jù)庫(kù)中的數(shù)據(jù)流,但通常我們不需要這樣做,因?yàn)閿?shù)據(jù)庫(kù)將分派更改。另一方面,依靠數(shù)據(jù)庫(kù)來(lái)調(diào)度變化將依賴(lài)于不好的副作用,因?yàn)槿绻麛?shù)據(jù)沒(méi)有變化,數(shù)據(jù)庫(kù)可以避免調(diào)度變化,那么它可能會(huì)中斷。我們也不想派遣從網(wǎng)絡(luò)到達(dá)的結(jié)果,因?yàn)檫@將違背單一的事實(shí)來(lái)源(也許在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變保存的值)。我們也不希望SUCCESS沒(méi)有新的數(shù)據(jù),因?yàn)樗鼤?huì)向客戶(hù)發(fā)送錯(cuò)誤的信息。
以下是NetworkBoundResource班級(jí)為其子女提供的公共API :
// 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)注意,上面的類(lèi)定義了兩個(gè)類(lèi)型參數(shù)(ResultType, RequestType),因?yàn)閺腁PI返回的數(shù)據(jù)類(lèi)型可能與本地使用的數(shù)據(jù)類(lèi)型不匹配。
另請(qǐng)注意,上面的代碼ApiResponse用于網(wǎng)絡(luò)請(qǐng)求。 ApiResponse是一個(gè)簡(jiǎn)單的Retrofit2.Call類(lèi)包裝,將其響應(yīng)轉(zhuǎn)換為L(zhǎng)iveData。
以下是該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來(lái)User在存儲(chǔ)庫(kù)中寫(xiě)入我們的磁盤(pán)和網(wǎng)絡(luò)綁定 實(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();
}
}
請(qǐng)點(diǎn)贊!因?yàn)槟愕墓膭?lì)是我寫(xiě)作的最大動(dòng)力!
歡迎關(guān)注 Lee-Wang 的簡(jiǎn)書(shū)!
不定期分享關(guān)于安卓開(kāi)發(fā)的干貨,讓堅(jiān)持成為一種習(xí)慣
冰凍三尺,非一日之寒
人生最可悲的事,莫過(guò)于胸懷大志,卻又虛度光陰!!!
不要在該奮斗的年紀(jì)選擇了安逸
生命不息,奮斗不止,萬(wàn)事起于忽微,量變引起質(zhì)變
每當(dāng)你在感嘆,如果有這樣一個(gè)東西就好了的時(shí)候,請(qǐng)注意,其實(shí)這是你的機(jī)會(huì)
人不會(huì)死在絕境,卻往往栽在十字路口
知道+做到=得到