本指南適用于過去構(gòu)建應(yīng)用程序基礎(chǔ)知識(shí)的開發(fā)人員,現(xiàn)在希望了解構(gòu)建強(qiáng)大的,生產(chǎn)質(zhì)量的應(yīng)用程序的最佳實(shí)踐和建議的體系結(jié)構(gòu)。
本頁假定您熟悉Android Framework。 如果您不熟悉應(yīng)用程序開發(fā),請查看入門培訓(xùn)系列,其中包含本指南的先決條件主題。
一、應(yīng)用開發(fā)者面臨的常見問題
與傳統(tǒng)的桌面版本不同,在大多數(shù)情況下,它們只有一個(gè)來自啟動(dòng)器快捷方式的入口點(diǎn)并作為單個(gè)整體流程運(yùn)行,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多。典型的Android應(yīng)用程序由多個(gè)應(yīng)用程序組件構(gòu)成,包括活動(dòng),片段,服務(wù),內(nèi)容提供程序和廣播接收器。
大多數(shù)這些應(yīng)用程序組件都在應(yīng)用程序清單中聲明,Android操作系統(tǒng)使用該清單來決定如何將您的應(yīng)用程序集成到其設(shè)備的整體用戶體驗(yàn)中。雖然如前所述,桌面應(yīng)用程序傳統(tǒng)上是一個(gè)單一的流程,但是正確編寫的Android應(yīng)用程序需要更加靈活,因?yàn)橛脩粼谠O(shè)備上編寫不同的應(yīng)用程序,不斷切換流程和任務(wù)。
例如,考慮當(dāng)您在自己喜歡的社交網(wǎng)絡(luò)應(yīng)用中分享照片時(shí)會(huì)發(fā)生什么。
- 該應(yīng)用程序觸發(fā)相機(jī)意圖,Android操作系統(tǒng)啟動(dòng)相機(jī)應(yīng)用程序來處理請求。此時(shí),用戶離開社交網(wǎng)絡(luò)應(yīng)用程序,但他們的體驗(yàn)是無縫的。
- 反過來,相機(jī)應(yīng)用程序可能會(huì)觸發(fā)其他意圖,例如啟動(dòng)文件選擇器,這可能會(huì)啟動(dòng)另一個(gè)應(yīng)用程序。
- 最終用戶回到社交網(wǎng)絡(luò)應(yīng)用程序并共享照片。
此外,在此過程中的任何時(shí)刻,用戶都可能被電話打斷,并在完成電話呼叫后回來分享照片。
在Android中,這種跳頻行為很常見,因此您的應(yīng)用必須正確處理這些流量。請記住,移動(dòng)設(shè)備受資源限制,因此在任何時(shí)候,操作系統(tǒng)都可能需要?dú)⑺滥承?yīng)用程序以為新設(shè)備騰出空間。
所有這一切的重點(diǎn)在于,您的應(yīng)用程序組件可以單獨(dú)啟動(dòng),也可以無序啟動(dòng),并且可以由用戶或系統(tǒng)隨時(shí)銷毀。由于應(yīng)用程序組件是短暫的,并且它們的生命周期(當(dāng)它們被創(chuàng)建和銷毀時(shí))不在您的控制之下,因此您不應(yīng)在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用程序數(shù)據(jù)或狀態(tài),并且您的應(yīng)用程序組件不應(yīng)相互依賴。
二、共同的構(gòu)建原則
如果您無法使用應(yīng)用程序組件來存儲(chǔ)應(yīng)用程序數(shù)據(jù)和狀態(tài),那么應(yīng)該如何構(gòu)建應(yīng)用程序?
您應(yīng)該關(guān)注的最重要的事情是在您的應(yīng)用中分離關(guān)注點(diǎn)。在Activity或Fragment中編寫所有代碼是一個(gè)常見的錯(cuò)誤。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中。盡可能保持精簡可以避免許多與生命周期相關(guān)的問題。不要忘記你沒有這些類,它們只是膠水類,體現(xiàn)了操作系統(tǒng)和你的應(yīng)用程序之間的契約。 Android操作系統(tǒng)可能會(huì)根據(jù)用戶交互或其他因素(如內(nèi)存不足)隨時(shí)銷毀它們。最好盡量減少對它們的依賴,以提供可靠的用戶體驗(yàn)。
第二個(gè)重要原則是您應(yīng)該從模型中驅(qū)動(dòng)UI,最好是持久模型。持久性是理想的兩個(gè)原因:如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源,您的用戶將不會(huì)丟失數(shù)據(jù),即使網(wǎng)絡(luò)連接不穩(wěn)定或未連接,您的應(yīng)用程序也將繼續(xù)工作。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件。它們獨(dú)立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問題隔離開來。保持UI代碼簡單且沒有應(yīng)用程序邏輯,使其更易于管理。將您的應(yīng)用程序基于具有明確定義的數(shù)據(jù)管理職責(zé)的模型類,將使其可測試且您的應(yīng)用程序保持一致。
三、推薦的應(yīng)用架構(gòu)
在本節(jié)中,我們將演示如何使用Architecture Components通過用例來構(gòu)建應(yīng)用程序。
注意:沒有一種方法可以編寫最適合每種情況的應(yīng)用程序。 話雖這么說,這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個(gè)很好的起點(diǎn)。 如果您已經(jīng)有了編寫Android應(yīng)用程序的好方法,則無需更改。
想象一下,我們正在構(gòu)建一個(gè)顯示用戶配置文件的UI。 此用戶配置文件將使用REST API從我們自己的私有后端獲取。
概述
首先,請考慮下圖,該圖顯示了在設(shè)計(jì)應(yīng)用程序后所有模塊應(yīng)如何相互交互:

請注意,每個(gè)組件僅取決于其下一級(jí)的組件。 例如,活動(dòng)和片段僅依賴于視圖模型。 存儲(chǔ)庫是唯一依賴于其他多個(gè)類的類; 在此示例中,存儲(chǔ)庫依賴于持久數(shù)據(jù)模型和遠(yuǎn)程后端數(shù)據(jù)源。
這種設(shè)計(jì)創(chuàng)造了一致和愉快的用戶體驗(yàn)。 無論用戶在上次關(guān)閉應(yīng)用程序幾分鐘后還是幾天后都回到應(yīng)用程序,他們會(huì)立即看到應(yīng)用程序在本地持續(xù)存在的用戶信息。 如果此數(shù)據(jù)過時(shí),應(yīng)用程序的存儲(chǔ)庫模塊將開始在后臺(tái)更新數(shù)據(jù)。
構(gòu)建用戶界面
UI將包含一個(gè)片段UserProfileFragment.java及其對應(yīng)的布局文件user_profile_layout.xml。
為了驅(qū)動(dòng)UI,我們的數(shù)據(jù)模型需要包含兩個(gè)數(shù)據(jù)元素。
- 用戶
ID:用戶的標(biāo)識(shí)符。 最好使用片段參數(shù)將此信息傳遞到片段中。 如果Android操作系統(tǒng)破壞了您的進(jìn)程,則會(huì)保留此信息,以便下次重新啟動(dòng)應(yīng)用時(shí)ID可用。
-User對象:保存用戶數(shù)據(jù)的POJO。
我們將基于ViewModel類創(chuàng)建一個(gè)UserProfileViewModel來保存這些信息。
ViewModel為特定UI組件(例如片段或活動(dòng))提供數(shù)據(jù),并處理與數(shù)據(jù)處理的業(yè)務(wù)部分的通信,例如調(diào)用其他組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改。ViewModel不了解View,也不受配置更改的影響,例如由于輪換而重新創(chuàng)建活動(dòng)。
現(xiàn)在我們有3個(gè)文件。
-
user_profile.xml:屏幕的UI定義。 -
UserProfileViewModel.java:為UI準(zhǔn)備數(shù)據(jù)的類。 -
UserProfileFragment.java:UI控制器,它在ViewModel中顯示數(shù)據(jù)并對用戶交互作出反應(yīng)。
下面是我們的開始實(shí)現(xià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)設(shè)置ViewModel的用戶字段時(shí),我們需要一種方法來通知UI。 這是LiveData類的用武之地。
LiveData是一個(gè)可觀察的數(shù)據(jù)持有者。 它允許應(yīng)用程序中的組件觀察LiveData對象的更改,而無需在它們之間創(chuàng)建明確且嚴(yán)格的依賴關(guān)系路徑。 LiveData還尊重應(yīng)用程序組件(活動(dòng),片段,服務(wù))的生命周期狀態(tài),并做正確的事情來防止對象泄漏,以便您的應(yīng)用程序不會(huì)消耗更多內(nèi)存。
注意:如果您已經(jīng)在使用像
RxJava或Agera這樣的庫,則可以繼續(xù)使用它們而不是LiveData。 但是當(dāng)您使用它們或其他方法時(shí),請確保正確處理生命周期,以便在相關(guān)LifecycleOwner停止時(shí)數(shù)據(jù)流暫停,并在銷毀LifecycleOwner時(shí)銷毀流。 您還可以添加android.arch.lifecycle:reactivestreams工件以將LiveData與另一個(gè)反應(yīng)流庫(例如,RxJava2)一起使用。
現(xiàn)在,我們使用LiveData <User>替換UserProfileViewModel中的User字段,以便在更新數(shù)據(jù)時(shí)通知片段。 LiveData的優(yōu)點(diǎn)在于它具有生命周期意識(shí),并且在不再需要時(shí)會(huì)自動(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)的其他庫,您可能已經(jīng)意識(shí)到我們不必覆蓋片段的onStop()方法來停止觀察數(shù)據(jù)。 LiveData不需要這樣做,因?yàn)樗梢宰R(shí)別生命周期,這意味著除非片段處于活動(dòng)狀態(tài)(收到onStart()但沒有收到onStop()),否則它不會(huì)調(diào)用回調(diào)。當(dāng)片段收到onDestroy()時(shí),LiveData也會(huì)自動(dòng)刪除觀察者。
我們也沒有做任何特殊處理配置更改(例如,用戶旋轉(zhuǎn)屏幕)。 ViewModel會(huì)在配置更改時(shí)自動(dòng)恢復(fù),因此只要新片段生效,它就會(huì)收到相同的ViewModel實(shí)例,并且會(huì)立即使用當(dāng)前數(shù)據(jù)調(diào)用回調(diào)。這就是ViewModels不應(yīng)該直接引用Views的原因;它們可以比View的生命周期更長久。請參閱ViewModel的生命周期。
獲取數(shù)據(jù)
現(xiàn)在我們將ViewModel連接到片段,但ViewModel如何獲取用戶數(shù)據(jù)? 在此示例中,我們假設(shè)我們的后端提供REST API。 我們將使用Retrofit庫來訪問我們的后端,盡管您可以自由地使用不同的庫來實(shí)現(xiàn)相同的目的。
這是我們的改造Web服務(wù),它與我們的后端通信:
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);
}
實(shí)現(xiàn)ViewModel的第一個(gè)想法可能涉及直接調(diào)用Webservice來獲取數(shù)據(jù)并將其分配回用戶對象。 即使它有效,您的應(yīng)用程序也會(huì)隨著它的增長而難以維護(hù)。 它對ViewModel類負(fù)有太多責(zé)任,這違背了我們之前提到的關(guān)注點(diǎn)分離原則。 此外,ViewModel的范圍與活動(dòng)或片段生命周期相關(guān)聯(lián),因此在其生命周期結(jié)束時(shí)丟失所有數(shù)據(jù)是一種糟糕的用戶體驗(yàn)。 相反,我們的ViewModel會(huì)將此工作委托給新的Repository模塊。
存儲(chǔ)庫模塊負(fù)責(zé)處理數(shù)據(jù)操作。 它們?yōu)閼?yīng)用程序的其余部分提供了一個(gè)干凈的
API。 他們知道從何處獲取數(shù)據(jù)以及在更新數(shù)據(jù)時(shí)要進(jìn)行的API調(diào)用。 您可以將它們視為不同數(shù)據(jù)源(持久模型,Web服務(wù),緩存等)之間的中介。
下面的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;
}
}
即使存儲(chǔ)庫模塊看起來不必要,它也是一個(gè)重要的目的; 它從應(yīng)用程序的其余部分抽象出數(shù)據(jù)源。 現(xiàn)在我們的ViewModel不知道Webservice獲取數(shù)據(jù),這意味著我們可以根據(jù)需要將其交換為其他實(shí)現(xiàn)。
注意:為簡單起見,我們省略了網(wǎng)絡(luò)錯(cuò)誤情況。 有關(guān)公開錯(cuò)誤和加載狀態(tài)的替代實(shí)現(xiàn),請參閱附錄:公開網(wǎng)絡(luò)狀態(tài)。
管理組件之間的依賴關(guān)系:
上面的UserRepository類需要一個(gè)Webservice實(shí)例來完成它的工作。 它可以簡單地創(chuàng)建它,但要做到這一點(diǎn),它還需要知道Webservice類的依賴關(guān)系來構(gòu)建它。 這將使代碼顯著復(fù)雜化和復(fù)制(例如,需要Webservice實(shí)例的每個(gè)類都需要知道如何使用其依賴關(guān)系來構(gòu)造它)。 此外,UserRepository可能不是唯一需要Web服務(wù)的類。 如果每個(gè)類都創(chuàng)建一個(gè)新的WebService,那么它將非常耗費(fèi)資源。
您可以使用兩種模式來解決此問題:
- 依賴注入:依賴注入允許類在不構(gòu)造它們的情況下定義它們的依賴關(guān)系。 在運(yùn)行時(shí),另一個(gè)類負(fù)責(zé)提供這些依賴項(xiàng)。 我們建議使用
Google的Dagger 2庫來實(shí)現(xiàn)Android應(yīng)用中的依賴注入。Dagger 2通過遍歷依賴關(guān)系樹自動(dòng)構(gòu)造對象,并為依賴關(guān)系提供編譯時(shí)保證。 - 服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊表,其中類可以獲取它們的依賴關(guān)系而不是構(gòu)造它們。 它比依賴注入(
DI)更容易實(shí)現(xiàn),因此如果您不熟悉DI,請使用服務(wù)定位器。
這些模式允許您擴(kuò)展代碼,因?yàn)樗鼈兲峁┝饲逦哪J絹砉芾硪蕾図?xiàng),而無需復(fù)制代碼或增加復(fù)雜性。 它們都允許交換實(shí)現(xiàn)進(jìn)行測試; 這是使用它們的主要好處之一。
在此示例中,我們將使用Dagger 2來管理依賴項(xiàng)。
連接ViewModel和存儲(chǔ)庫
現(xiàn)在我們修改UserProfileViewModel以使用存儲(chǔ)庫。
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ǔ)庫實(shí)現(xiàn)適用于抽象Web服務(wù)的調(diào)用,但因?yàn)樗灰蕾囉谝粋€(gè)數(shù)據(jù)源,所以它不是很有用。
上面的UserRepository實(shí)現(xiàn)的問題是,在獲取數(shù)據(jù)之后,它不會(huì)將其保留在任何位置。 如果用戶離開UserProfileFragment并返回到它,則應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)。 這有兩個(gè)原因:它浪費(fèi)了寶貴的網(wǎng)絡(luò)帶寬并迫使用戶等待新查詢完成。 為了解決這個(gè)問題,我們將向UserRepository添加一個(gè)新的數(shù)據(jù)源,用于將User對象緩存在內(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)榇鎯?chǔ)庫從內(nèi)存緩存中檢索數(shù)據(jù)。但是,如果用戶離開應(yīng)用程序并在Android操作系統(tǒng)殺死進(jìn)程后數(shù)小時(shí)后回來會(huì)發(fā)生什么?
通過當(dāng)前的實(shí)現(xiàn),我們需要從網(wǎng)絡(luò)中再次獲取數(shù)據(jù)。這不僅是一種糟糕的用戶體驗(yàn),而且還浪費(fèi),因?yàn)樗鼘⑹褂靡苿?dòng)數(shù)據(jù)重新獲取相同的數(shù)據(jù)。您可以通過緩存Web請求來解決此問題,但這會(huì)產(chǎn)生新問題。如果相同的用戶數(shù)據(jù)顯示來自另一種類型的請求(例如,獲取朋友列表)會(huì)發(fā)生什么?然后,您的應(yīng)用可能會(huì)顯示不一致的數(shù)據(jù),這充其量只是令人困惑的用戶體驗(yàn)。例如,相同用戶的數(shù)據(jù)可能不同地顯示,因?yàn)榭梢栽诓煌瑫r(shí)間執(zhí)行朋友列表請求和用戶請求。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)。
處理此問題的正確方法是使用持久模型。這是Room persistence library來救援的地方。
Room是一個(gè)對象映射庫,它提供本地?cái)?shù)據(jù)持久性和最少的樣板代碼。 在編譯時(shí),它根據(jù)模式驗(yàn)證每個(gè)查詢,以便損壞的SQL查詢導(dǎo)致編譯時(shí)錯(cuò)誤而不是運(yùn)行時(shí)失敗。Room抽象了使用原始SQL表和查詢的一些底層實(shí)現(xiàn)細(xì)節(jié)。 它還允許觀察對數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改,通過LiveData對象公開此類更改。 此外,它還明確定義了解決常見問題的線程約束,例如訪問主線程上的存儲(chǔ)。
注意:如果您的應(yīng)用已使用其他持久性解決方案(如
SQLite對象關(guān)系映射(ORM)),則無需使用Room替換現(xiàn)有解決方案。 但是,如果您正在編寫新應(yīng)用或重構(gòu)現(xiàn)有應(yīng)用,我們建議您使用Room來保留應(yīng)用的數(shù)據(jù)。 這樣,您就可以利用庫的抽象和查詢驗(yàn)證功能。
要使用Room,我們需要定義本地模式。 首先,使用@Entity注釋User類,將其標(biāo)記為數(shù)據(jù)庫中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,通過為您的應(yīng)用擴(kuò)展RoomDatabase來創(chuàng)建數(shù)據(jù)庫類:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
請注意,MyDatabase是抽象的。 Room自動(dòng)提供它的實(shí)現(xiàn)。 有關(guān)詳細(xì)信息,請參閱房間文檔
現(xiàn)在我們需要一種將用戶數(shù)據(jù)插入數(shù)據(jù)庫的方法。 為此,我們將創(chuàng)建一個(gè)數(shù)據(jù)訪問對象(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ù)庫類中引用DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請注意,load方法返回LiveData <User>。 會(huì)議室知道數(shù)據(jù)庫何時(shí)被修改,并且在數(shù)據(jù)發(fā)生變化時(shí)會(huì)自動(dòng)通知所有活動(dòng)的觀察者。 因?yàn)樗褂?code>LiveData,所以這將是有效的,因?yàn)橹挥性谥辽儆幸粋€(gè)活動(dòng)觀察者的情況下它才會(huì)更新數(shù)據(jù)。
注意:
Room根據(jù)表格修改檢查失效,這意味著它可能會(huì)發(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());
}
});
}
}
請注意,即使我們更改了UserRepository中數(shù)據(jù)的來源,我們也不需要更改UserProfileViewModel或UserProfileFragment。這是抽象提供的靈活性。這對于測試也很有用,因?yàn)槟梢栽跍y試UserProfileViewModel時(shí)提供虛假的UserRepository。
現(xiàn)在我們的代碼已經(jīng)完成。如果用戶幾天后回到相同的用戶界面,他們會(huì)立即看到用戶信息,因?yàn)槲覀円呀?jīng)保留了用戶信息。同時(shí),如果數(shù)據(jù)陳舊,我們的存儲(chǔ)庫將在后臺(tái)更新數(shù)據(jù)。當(dāng)然,根據(jù)您的使用情況,如果持久數(shù)據(jù)太舊,您可能不希望顯示這些數(shù)據(jù)。
在某些用例中,例如pull-to-refresh,如果當(dāng)前正在進(jìn)行網(wǎng)絡(luò)操作,則UI必須向用戶顯示。將UI操作與實(shí)際數(shù)據(jù)分開是一種很好的做法,因?yàn)樗赡芤蚋鞣N原因而更新(例如,如果我們獲取朋友列表,則可能會(huì)再次獲取相同的用戶,從而觸發(fā)LiveData <User>更新)。從UI的角度來看,飛行中有請求的事實(shí)只是另一個(gè)數(shù)據(jù)點(diǎn),類似于任何其他數(shù)據(jù)點(diǎn)(如User對象)。
這個(gè)用例有兩種常見的解決方案:
- 更改getUser以返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。 附錄中提供了一個(gè)示例實(shí)現(xiàn):公開網(wǎng)絡(luò)狀態(tài)部分。
- 在存儲(chǔ)庫類中提供另一個(gè)可以返回用戶刷新狀態(tài)的公共函數(shù)。 如果要僅在響應(yīng)顯式用戶操作(如pull-to-refresh)時(shí)在UI中顯示網(wǎng)絡(luò)狀態(tài),則此選項(xiàng)更好。
單一的事實(shí)來源
不同的REST API端點(diǎn)通常會(huì)返回相同的數(shù)據(jù)。 例如,如果我們的后端有另一個(gè)返回朋友列表的端點(diǎn),則同一個(gè)用戶對象可能來自兩個(gè)不同的API端點(diǎn),可能是不同的粒度。 如果UserRepository按原樣從Webservice請求返回響應(yīng),則我們的UI可能會(huì)顯示不一致的數(shù)據(jù),因?yàn)檫@些請求之間的數(shù)據(jù)可能會(huì)在服務(wù)器端發(fā)生更改。 這就是為什么在UserRepository實(shí)現(xiàn)中,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫中。 然后,對數(shù)據(jù)庫的更改將觸發(fā)活動(dòng)LiveData對象的回調(diào)。
在此模型中,數(shù)據(jù)庫充當(dāng)單一事實(shí)來源,應(yīng)用程序的其他部分通過存儲(chǔ)庫訪問它。 無論您是否使用磁盤緩存,我們都建議您的存儲(chǔ)庫將數(shù)據(jù)源指定為應(yīng)用程序其余部分的唯一真實(shí)來源。
測試
我們已經(jīng)提到分離的一個(gè)好處是可測試性。讓我們看看我們?nèi)绾螠y試每個(gè)代碼模塊。
用戶界面和交互:這是您唯一需要Android UI Instrumentation測試的時(shí)間。測試UI代碼的最佳方法是創(chuàng)建Espresso測試。您可以創(chuàng)建片段并為其提供模擬ViewModel。由于片段只與ViewModel對話,因此模擬它就足以完全測試此UI。
ViewModel:可以使用JUnit測試來測試ViewModel。您只需要模擬UserRepository來測試它。
UserRepository:您也可以使用JUnit測試來測試UserRepository。您需要模擬Web服務(wù)和DAO。您可以測試它是否進(jìn)行了正確的Web服務(wù)調(diào)用,將結(jié)果保存到數(shù)據(jù)庫中,并且如果數(shù)據(jù)被緩存并且是最新的,則不會(huì)發(fā)出任何不必要的請求。由于Webservice和UserDao都是接口,因此您可以模擬它們或?yàn)楦鼜?fù)雜的測試用例創(chuàng)建虛假實(shí)現(xiàn)。
UserDao:測試DAO類的推薦方法是使用檢測測試。由于這些檢測測試不需要任何UI,因此它們?nèi)匀豢梢钥焖龠\(yùn)行。對于每個(gè)測試,您可以創(chuàng)建內(nèi)存數(shù)據(jù)庫以確保測試沒有任何副作用(例如更改磁盤上的數(shù)據(jù)庫文件)。
Room還允許指定數(shù)據(jù)庫實(shí)現(xiàn),因此您可以通過提供SupportSQLiteOpenHelper的JUnit實(shí)現(xiàn)來測試它。通常不建議使用此方法,因?yàn)樵O(shè)備上運(yùn)行的SQLite版本可能與主機(jī)上的SQLite版本不同。Web服務(wù):重要的是使測試獨(dú)立于外部世界,因此即使您的Webservice測試也應(yīng)該避免對您的后端進(jìn)行網(wǎng)絡(luò)調(diào)用。有很多庫可以幫助解決這個(gè)問題。例如,MockWebServer是一個(gè)很棒的庫,可以幫助您為測試創(chuàng)建虛假的本地服務(wù)器。
-
測試工件架構(gòu)組件提供了一個(gè)maven工件來控制其后臺(tái)線程。在android.arch.core:核心測試工件中,有2個(gè)JUnit規(guī)則:
- InstantTaskExecutorRule:此規(guī)則可用于強(qiáng)制Architecture Components立即在調(diào)用線程上執(zhí)行任何后臺(tái)操作。 - CountingTaskExecutorRule:此規(guī)則可用于檢測測試,以等待架構(gòu)組件的后臺(tái)操作或?qū)⑵渥鳛榭臻e資源連接到Espresso。
最終的架構(gòu)
下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>

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

它首先觀察資源的數(shù)據(jù)庫。 當(dāng)?shù)谝淮螐臄?shù)據(jù)庫加載條目時(shí),NetworkBoundResource會(huì)檢查結(jié)果是否足以分派和/或是否應(yīng)從網(wǎng)絡(luò)中獲取。 請注意,這兩個(gè)都可以同時(shí)發(fā)生,因?yàn)槟赡芟M趶木W(wǎng)絡(luò)更新緩存數(shù)據(jù)時(shí)顯示緩存數(shù)據(jù)。
如果網(wǎng)絡(luò)調(diào)用成功完成,它會(huì)將響應(yīng)保存到數(shù)據(jù)庫中并重新初始化流。 如果網(wǎng)絡(luò)請求失敗,我們會(huì)直接發(fā)送故障。
注意:將新數(shù)據(jù)保存到磁盤后,我們會(huì)重新初始化數(shù)據(jù)庫中的流,但通常我們不需要這樣做,因?yàn)閿?shù)據(jù)庫將調(diào)度更改。 另一方面,依靠數(shù)據(jù)庫來調(diào)度更改將依賴于副作用,這是不好的,因?yàn)槿绻麛?shù)據(jù)沒有改變,數(shù)據(jù)庫可以避免調(diào)度更改,它可能會(huì)中斷。 我們也不希望調(diào)度從網(wǎng)絡(luò)到達(dá)的結(jié)果,因?yàn)檫@將違背單一事實(shí)來源(可能在數(shù)據(jù)庫中存在將更改保存值的觸發(fā)器)。 我們也不想在沒有新數(shù)據(jù)的情況下發(fā)送SUCCESS,因?yàn)樗鼤?huì)向客戶端發(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();
}
請注意,上面的類定義了兩個(gè)類型參數(shù)(ResultType,RequestType),因?yàn)閺?code>API返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配。
另請注意,上面的代碼使用ApiResponse進(jìn)行網(wǎng)絡(luò)請求。 ApiResponse是一個(gè)簡單的Retrofit2.Call類包裝器,用于將其響應(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();
// 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在存儲(chǔ)庫中編寫磁盤和網(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();
}
}