本指南適用于那些過去構(gòu)建應(yīng)用程序有基礎(chǔ)知識派阱,現(xiàn)在想知道構(gòu)建強大的生產(chǎn)質(zhì)量應(yīng)用程序最佳實踐和建議的體系結(jié)構(gòu)的開發(fā)人員偎巢。
注意:本指南假設(shè)讀者熟悉Android框架爽茴。如果您不熟悉應(yīng)用程序開發(fā)咆霜,請查看入門培訓(xùn)系列籍铁,其中包含本指南的必備主題狐树。
APP開發(fā)者面臨的常見問題
與傳統(tǒng)的桌面應(yīng)用程序不同涮母,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多挟冠,在大多數(shù)情況下鹏漆,它們只有一個啟動快捷方式的入口點巩梢,并且可以作為一個單一的整體進程運行。一個典型的Android應(yīng)用程序是由多個應(yīng)用程序組件構(gòu)成的艺玲,包括活動括蝠,片段,服務(wù)饭聚,內(nèi)容提供者和廣播接收器忌警。
大多數(shù)這些應(yīng)用程序組件都是在Android操作系統(tǒng)使用的應(yīng)用程序清單中聲明的??,清單決定如何將您的應(yīng)用程序與其設(shè)備的整體用戶體驗的集成若治。如前所述慨蓝,桌面應(yīng)用程序傳統(tǒng)上是以整體的方式運行感混,但正確的Android應(yīng)用程序編寫需要更加靈活,因為用戶可以在設(shè)備上的不同應(yīng)用程序不斷切換流程和任務(wù)礼烈。
例如弧满,請考慮在您最喜愛的社交網(wǎng)絡(luò)應(yīng)用程序中分享照片時會發(fā)生什么情況。該應(yīng)用程序觸發(fā)Android操作系統(tǒng)此熬,啟動相機應(yīng)用程序來處理請求的相機意圖庭呜。此時,用戶離開了社交網(wǎng)絡(luò)應(yīng)用犀忱,但他們的體驗是無縫的募谎。相機應(yīng)用程序又可能觸發(fā)其他意圖,例如啟動文件選擇器阴汇,該文件選擇器可以啟動另一個應(yīng)用程序数冬。最終用戶回到社交網(wǎng)絡(luò)應(yīng)用程序并分享照片。此外搀庶,用戶在這個過程的任何時候都可能被電話打斷拐纱,并在打完電話后回來分享照片。
在Android中哥倔,這種應(yīng)用程序跳轉(zhuǎn)行為很常見秸架,所以您的應(yīng)用程序必須正確處理這些流程。請記住咆蒿,移動設(shè)備是資源受限东抹,所以在任何時候,操作系統(tǒng)可能需要殺死一些應(yīng)用程序沃测,以騰出空間給新的的應(yīng)用或進程缭黔。所有這一切的關(guān)鍵是,您的應(yīng)用程序組件可以單獨和無序地啟動芽突,并可以在任何時候由用戶或系統(tǒng)銷毀试浙。由于應(yīng)用程序組件是短暫的董瞻,它們的生命周期(創(chuàng)建和銷毀時)不在您的控制之下寞蚌,因此您不應(yīng)該在應(yīng)用程序組件中存儲任何應(yīng)用程序數(shù)據(jù)或狀態(tài),并且應(yīng)用程序組件不應(yīng)相互依賴钠糊。
常見的架構(gòu)原則
如果您不能使用應(yīng)用程序組件來存儲應(yīng)用程序數(shù)據(jù)和狀態(tài)挟秤,應(yīng)該如何構(gòu)建應(yīng)用程序?
你應(yīng)該關(guān)注的最重要的事情是在你的應(yīng)用程序中抄伍,將所有代碼寫入Activity或Fragment是一個常見的錯誤艘刚。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中。盡可能保持精簡可以避免許多生命周期相關(guān)的問題截珍。記住攀甚,你不擁有這些類箩朴,它們只是體現(xiàn)操作系統(tǒng)和你的應(yīng)用程序之間的契約的粘合劑。 Android操作系統(tǒng)可能會隨時根據(jù)用戶交互或其他因素(如低內(nèi)存)來銷毀它們秋度。最好最大限度地減少對他們的依賴炸庞,以提供可靠的用戶體驗。
第二個重要的原則是你應(yīng)該從一個模型驅(qū)動(數(shù)據(jù))你的UI荚斯,最好是一個持久模型埠居。持久性是理想的,原因有兩個:如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源事期,則您的用戶不會丟失數(shù)據(jù)滥壕,即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,您的應(yīng)用程序也將繼續(xù)工作兽泣。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件绎橘。它們獨立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問題是隔離的唠倦。保持簡單的UI代碼和減少的應(yīng)用程序邏輯金踪,使管理更容易。將您的應(yīng)用程序放在具有明確定義的管理數(shù)據(jù)的模型類上牵敷,使它們可測試胡岔,并能使您的應(yīng)用程序保持一致。
推薦的應(yīng)用架構(gòu)
在本節(jié)中枷餐,我們將演示如何通過使用用例來構(gòu)建使用體系結(jié)構(gòu)組件的應(yīng)用程序靶瘸。
注意:不可能有一種編寫應(yīng)用程序的方法,對每種情況都是最好的毛肋。也就是說怨咪,這個推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個很好的起點。如果您已經(jīng)有了編寫Android應(yīng)用的好方法润匙,則不需要更改诗眨。想象一下,我們正在構(gòu)建一個顯示用戶配置文件的用戶界面孕讳。該用戶配置文件將使用REST API從我們自己的后端獲取數(shù)據(jù)匠楚。
假如,我們正在構(gòu)建一個顯示用戶信息的用戶界面厂财。該用戶信息將使用REST API從我們自己的后端獲取芋簿。
構(gòu)建用戶界面
用戶界面將由fragment UserProfileFragment.java 和 布局文件user_profile_layout.xml組成。
為了驅(qū)動用戶界面璃饱,我們的數(shù)據(jù)模型需要保存兩個數(shù)據(jù)元素与斤。
User ID:用戶的標(biāo)識符。最好使用Fragment參數(shù)將此信息傳遞到Fragment中。如果Android操作系統(tǒng)破壞您的進程撩穿,這些信息將被保留磷支,以便在您的應(yīng)用下次重新啟動時可用。
User對象:持有用戶數(shù)據(jù)的POJO
我們將創(chuàng)建一個基于ViewModel類的UserProfileViewModel來保存這些信息食寡。
ViewModel為特定的UI組件(如Fragment或Activity)提供數(shù)據(jù)齐唆,并處理與數(shù)據(jù)處理業(yè)務(wù)部分的通信,例如調(diào)用其他組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改冻河。 ViewModel不知道UI箍邮,并且不受配置更改的影響,例如由于旋轉(zhuǎn)而重新創(chuàng)建Activity叨叙。
現(xiàn)在我們有3個文件:
user_profile.xml
UserProfileViewModel.java
UserProfileFragment.java
下面是我們的開始的實現(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)在锭弊,我們有這三個代碼模塊,我們?nèi)绾芜B接它們擂错?畢竟味滞,當(dāng)ViewModel的用戶字段被設(shè)置,我們需要一種方式來通知用戶界面钮呀。這是使用LiveData類的地方剑鞍。
LiveData是一個可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData對象的更改爽醋,而不會在它們之間創(chuàng)建明確的和嚴(yán)格的依賴關(guān)系路徑蚁署。 LiveData還尊重您的應(yīng)用程序組件(活動,片段蚂四,服務(wù))的生命周期狀態(tài)光戈,并做正確的事情來防止對象泄漏,使您的應(yīng)用程序不消耗更多的內(nèi)存遂赠。
注意:如果您已經(jīng)在使用類似RxJava或Agera的庫久妆,則可以繼續(xù)使用它們而不是LiveData。但是跷睦,當(dāng)您使用它們或其他方法時筷弦,請確保正確處理生命周期,以便在相關(guān)的LifecycleOwner停止時停止數(shù)據(jù)流抑诸,并在銷毀LifecycleOwner時銷毀數(shù)據(jù)流烂琴。您還可以添加android.arch.lifecycle:reactivestreams工件以將LiveData與另一個反應(yīng)流庫(例如RxJava2)一起使用。
現(xiàn)在我們將UserProfileViewModel中的User字段替換為一個LiveData <User>哼鬓,以便在數(shù)據(jù)更新時通知這個分段监右。 LiveData最棒的地方在于它具有生命周期感知能力,當(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ù)時,都會調(diào)用onChanged回調(diào)称簿,并刷新UI扣癣。
如果您熟悉使用可觀察回調(diào)的其他庫,您可能已經(jīng)意識到憨降,我們不必重寫片段的onStop()方法來停止觀察數(shù)據(jù)父虑。這對于LiveData來說并不是必須的绣夺,因為它是生命周期感知的舆逃,這意味著它不會調(diào)用回調(diào),除非片段處于活動狀態(tài)(收到onStart()师倔,但沒有收到onStop())悔叽。當(dāng)片段收到onDestroy()時莱衩,LiveData也會自動移除觀察者。
我們也沒有做任何特殊的處理配置變化(例如娇澎,用戶旋轉(zhuǎn)屏幕)笨蚁。當(dāng)配置改變時,ViewModel會自動恢復(fù)趟庄,所以一旦新的片段生效括细,它將接收到同一個ViewModel的實例,回調(diào)將被立即調(diào)用當(dāng)前數(shù)據(jù)戚啥。這就是ViewModel不能直接引用Views的原因奋单。他們可以超越View的生命周期。請參閱ViewModel的生命周期猫十。
獲取數(shù)據(jù)
現(xiàn)在我們已經(jīng)將ViewModel連接到了片段辱匿,但是ViewModel如何獲取用戶數(shù)據(jù)呢?在這個例子中炫彩,我們假設(shè)我們的后端提供了一個REST API匾七。我們將使用Retrofit庫來訪問我們的后端,盡管您可以自由使用不同的庫來達到同樣的目的江兢。
這里是我們的改進的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);
}
ViewModel的一個原始的實現(xiàn)可以直接調(diào)用Web服務(wù)來獲取數(shù)據(jù)并將其分配給用戶對象昨忆。即使它可行,您的應(yīng)用程序也將難以維持杉允。它給ViewModel類提供了太多的責(zé)任邑贴,這違背了前面提到的分離原則。此外叔磷,ViewModel的范圍與活動或片段生命周期相關(guān)聯(lián)拢驾,因此,在生命周期結(jié)束時丟失所有數(shù)據(jù)是不好的用戶體驗改基。相反繁疤,我們的ViewModel將這個工作委托給一個新的Repository模塊。
Repository模塊負(fù)責(zé)處理數(shù)據(jù)操作。他們提供了一個干凈的API到應(yīng)用程序的其余部分稠腊。他們知道從何處獲取數(shù)據(jù)以及在更新數(shù)據(jù)時調(diào)用哪些API躁染。您可以將它們視為不同數(shù)據(jù)源(持久模型,Web服務(wù)架忌,緩存等)之間的中介吞彤。
下面的UserRepository類使用WebService來獲取用戶數(shù)據(jù)項:
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模塊看起來沒有必要,它也有著重要的作用叹放。它從應(yīng)用程序的其余部分提取數(shù)據(jù)源∈嗡。現(xiàn)在我們的ViewModel不知道數(shù)據(jù)是由Webservice獲取的,這意味著我們可以根據(jù)需要將其交換為其他實現(xiàn)井仰。
注意:為簡單起見埋嵌,我們忽略了網(wǎng)絡(luò)錯誤的情況。有關(guān)公開錯誤和加載狀態(tài)的替代實現(xiàn)糕档,請參閱附錄:公開網(wǎng)絡(luò)狀態(tài)莉恼。
管理組件之間的依賴關(guān)系:
上面的UserRepository類需要Webservice的一個實例來完成它的工作。它可以簡單地創(chuàng)建它速那,但要做到這一點俐银,它也需要知道Webservice類的依賴關(guān)系來構(gòu)造它。這會使代碼復(fù)雜化和重復(fù)(例如端仰,每個需要Webservice實例的類都需要知道如何用它的依賴關(guān)系來構(gòu)造它)捶惜。此外,UserRepository可能不是唯一需要Web服務(wù)的類荔烧。如果每個類創(chuàng)建一個新的WebService吱七,這將是非常資源沉重。
有兩種模式可以用來解決這個問題:
依賴注入:依賴注入允許類在不構(gòu)造它們的情況下定義它們的依賴關(guān)系鹤竭。在運行時踊餐,另一個類負(fù)責(zé)提供這些依賴關(guān)系。我們推薦Google的Dagger 2庫在Android應(yīng)用程序中實現(xiàn)依賴注入臀稚。 Dagger 2通過遍歷依賴關(guān)系樹來自動構(gòu)造對象吝岭,并為依賴關(guān)系提供編譯時間保證。
服務(wù)定位器:服務(wù)定位器提供了一個注冊表吧寺,類可以獲得它們的依賴而不是構(gòu)建它們窜管。實現(xiàn)起來比依賴注入(DI)更容易,所以如果你不熟悉DI稚机,可以使用Service Locator幕帆。
這些模式允許您擴展您的代碼,因為它們提供了用于管理依賴關(guān)系的清晰模式赖条,無需重復(fù)代碼或增加復(fù)雜性失乾。他們兩人也允許交換實現(xiàn)測試;這是使用它們的主要好處之一
在這個例子中常熙,我們將使用Dagger 2來管理依賴關(guān)系。
連接ViewModel和存儲庫
現(xiàn)在我們修改我們的UserProfileViewModel來使用倉庫仗扬。
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ù)
上面的存儲庫實現(xiàn)對抽象調(diào)用Web服務(wù)是有好處的症概,但是因為它只依賴于一個數(shù)據(jù)源蕾额,所以它不是很有用早芭。
上面的UserRepository實現(xiàn)的問題是,在獲取數(shù)據(jù)之后诅蝶,它不保留在任何地方退个。如果用戶離開UserProfileFragment并返回到該應(yīng)用程序,則應(yīng)用程序會重新獲取數(shù)據(jù)调炬。這是不好的语盈,原因有兩個:浪費寶貴的網(wǎng)絡(luò)帶寬并強制用戶等待新的查詢完成。為了解決這個問題缰泡,我們將添加一個新的數(shù)據(jù)源到我們的UserRepository中刀荒,它將把用戶對象緩存在內(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)前的實現(xiàn)中棘钞,如果用戶旋轉(zhuǎn)屏幕或離開并返回到應(yīng)用程序缠借,則現(xiàn)有UI將立即可見,因為存儲庫從內(nèi)存中緩存中檢索數(shù)據(jù)宜猜。但是泼返,如果用戶離開應(yīng)用程序,并在Android操作系統(tǒng)殺死該進程后數(shù)小時后回來姨拥,會發(fā)生什么绅喉?
在目前的實施中,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)叫乌。這不僅是一個糟糕的用戶體驗柴罐,而且會浪費,因為它會使用移動數(shù)據(jù)重新獲取相同的數(shù)據(jù)憨奸。您可以簡單地通過緩存Web請求來解決這個問題革屠,但是會產(chǎn)生新的問題。如果相同的用戶數(shù)據(jù)顯示來自另一種類型的請求(例如膀藐,獲取朋友列表)屠阻,會發(fā)生什么情況?那么你的應(yīng)用程序可能會顯示不一致的數(shù)據(jù)额各,這是一個混亂的用戶體驗充其量国觉。例如,由于好友列表請求和用戶請求可以在不同的時間執(zhí)行虾啦,所以相同用戶的數(shù)據(jù)可能會以不同的方式顯示麻诀。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)痕寓。
處理這個問題的正確方法是使用持久模型。這是Room持久性庫優(yōu)點的地方蝇闭。
Room是一個對象映射庫呻率,提供本地數(shù)據(jù)持久性和最小的樣板代碼。在編譯時呻引,它會根據(jù)模式驗證每個查詢礼仗,以便斷開的SQL查詢導(dǎo)致編譯時錯誤,而不是運行時失敗逻悠。Room抽象出一些使用原始SQL表和查詢的底層實現(xiàn)細(xì)節(jié)元践。它還允許觀察對數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改,通過LiveData對象公開這些更改童谒。另外单旁,它明確定義了解決常見問題的線程約束,例如訪問主線程上的存儲饥伊。
注意:如果您的應(yīng)用程序已經(jīng)使用另一個持久性解決方案(如SQLite對象關(guān)系映射(ORM))象浑,則不需要使用Room替換現(xiàn)有的解決方案。但是琅豆,如果您正在編寫新的應(yīng)用程序或重構(gòu)現(xiàn)有應(yīng)用程序愉豺,我們建議使用Room來保存應(yīng)用程序的數(shù)據(jù)。這樣趋距,您可以利用庫的抽象和查詢驗證功能粒氧。
要使用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)用程序擴展RoomDatabase來創(chuàng)建一個數(shù)據(jù)庫類:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意MyDatabase是抽象的。Room自動提供一個實施狼渊。有關(guān)詳細(xì)信息箱熬,請參見Room文檔
現(xiàn)在我們需要一種將用戶數(shù)據(jù)插入數(shù)據(jù)庫的方法。為此狈邑,我們將創(chuàng)建一個數(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>糕伐。Room知道數(shù)據(jù)庫何時被修改,當(dāng)數(shù)據(jù)改變時它會自動通知所有活動的觀察者蘸嘶。因為它使用的是LiveData良瞧,所以這將是有效的陪汽,因為它只會在至少有一個活動觀察者的情況下更新數(shù)據(jù)。
注意:Room根據(jù)表格修改檢查失效褥蚯,這意味著它可能發(fā)送誤報通知挚冤。
現(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。這是抽象提供的靈活性尘执。這對于測試也很好舍哄,因為在測試UserProfileViewModel的時候可以提供一個偽造的UserRepository宴凉。
現(xiàn)在我們的代碼是完整的誊锭。如果用戶以后回到相同的用戶界面,他們會立即看到用戶信息弥锄,因為我們持久化了丧靡。同時,如果數(shù)據(jù)陳舊籽暇,我們的倉庫將在后臺更新數(shù)據(jù)温治。當(dāng)然,根據(jù)您的使用情況戒悠,如果數(shù)據(jù)太舊熬荆,您可能不希望顯示持久數(shù)據(jù)。
在一些使用情況下绸狐,如下拉刷新卤恳,UI顯示用戶是否正在進行網(wǎng)絡(luò)操作是非常重要的。將UI操作與實際數(shù)據(jù)分開是一種很好的做法寒矿,因為它可能因各種原因而更新(例如突琳,如果我們獲取朋友列表,則可能會再次觸發(fā)同一用戶符相,觸發(fā)LiveData <User>更新)拆融。從用戶界面的角度來看,一個正在進行中請求只是另一個數(shù)據(jù)點啊终,類似于其他任何數(shù)據(jù)(如用戶對象)镜豹。
這個用例有兩個常見的解決方案:
更改getUser以返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。附錄中提供了一個示例實現(xiàn):公開網(wǎng)絡(luò)狀態(tài)部分蓝牲。
在存儲庫類中提供另一個可以返回用戶刷新狀態(tài)的公共函數(shù)趟脂。如果只想響應(yīng)顯式的用戶操作(如下拉刷新)來顯示網(wǎng)絡(luò)狀態(tài),則此選項更好搞旭。
單一的事實來源
不同的REST API端點通常返回相同的數(shù)據(jù)散怖。例如菇绵,如果我們的后端擁有另一個返回朋友列表的端點,則同一個用戶對象可能來自兩個不同的API端點镇眷,也許端點不同咬最。如果UserRepository原樣返回來自Webservice請求的響應(yīng),那么我們的UI可能會顯示不一致的數(shù)據(jù)欠动,因為這些請求之間的數(shù)據(jù)可能在服務(wù)器端發(fā)生更改永乌。這就是為什么在UserRepository實現(xiàn)中,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫中具伍。然后翅雏,對數(shù)據(jù)庫的更改將觸發(fā)活動LiveData對象上的回調(diào)。
在這個模型中人芽,數(shù)據(jù)庫充當(dāng)真實數(shù)據(jù)的單一來源望几,應(yīng)用程序的其他部分通過存儲庫訪問它。即使您使用磁盤緩存萤厅,都建議您將存儲庫數(shù)據(jù)源指定為應(yīng)用程序其余部分的單一來源橄抹。
測試
我們已經(jīng)提到分離的好處之一就是可測試性。讓我們看看我們?nèi)绾螠y試每個代碼模塊惕味。
用戶界面:這將是您唯一需要Android UI Instrumentation測試的時間楼誓。測試UI代碼的最好方法是創(chuàng)建一個Espresso測試。您可以創(chuàng)建片段并為其提供一個模擬的ViewModel名挥。由于該片段只與ViewModel交談疟羹,因此模擬它將足以完全測試此UI。
ViewModel: ViewModel可以使用JUnit測試進行測試禀倔。你只需要模擬UserRepository來測試它榄融。
UserRepository:您也可以使用JUnit測試來測試UserRepository。你需要模擬Web服務(wù)和DAO剃袍。您可以測試它是否進行正確的Web服務(wù)調(diào)用捎谨,將結(jié)果保存到數(shù)據(jù)庫中,如果數(shù)據(jù)已緩存且最新涛救,則不會發(fā)出任何不必要的請求。既然Webservice和UserDao都是接口舒萎,你可以模擬它們或者為更復(fù)雜的測試用例創(chuàng)建假實現(xiàn)。
UserDao: 測試DAO類的推薦方法是使用儀器測試臂寝。由于這些測試不需要任何用戶界面,他們?nèi)匀粫\行得很快咆贬。對于每個測試,您可以創(chuàng)建一個內(nèi)存數(shù)據(jù)庫掏缎,以確保測試沒有任何副作用(如更改磁盤上的數(shù)據(jù)庫文件)。Room還允許指定數(shù)據(jù)庫實現(xiàn)眷蜈,以便通過提供SupportSQLiteOpenHelper的JUnit實現(xiàn)來測試它沪哺。通常不建議使用這種方法,因為設(shè)備上運行的SQLite版本可能與主機上的SQLite版本不同酌儒。
Webservice:使測試獨立于外界是很重要的辜妓,所以即使是Web服務(wù)測試也應(yīng)該避免對后端進行網(wǎng)絡(luò)調(diào)用。有很多l(xiāng)ibrary可以幫助你今豆。例如嫌拣,MockWebServer是一個偉大的庫,可以幫助您為測試創(chuàng)建一個假的本地服務(wù)器呆躲。
-
Testing Artifacts: 架構(gòu)組件提供了一個maven工件來控制其后臺線程。在android.arch.core:核心測試工件內(nèi)部捶索,有2個JUnit規(guī)則:
- InstantTaskExecutorRule:此規(guī)則可用于強制架構(gòu)組件立即在調(diào)用線程上執(zhí)行任何后臺操作插掂。
- CountingTaskExecutorRule:此規(guī)則可用于檢測測試,以等待體系結(jié)構(gòu)組件的后臺操作或?qū)⑵渥鳛殚e置資源連接到Espresso腥例。
架構(gòu)圖
下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>
指導(dǎo)原則
編程是一個創(chuàng)造性的領(lǐng)域辅甥,構(gòu)建Android應(yīng)用程序也不是一個例外。解決問題的方法有很多種燎竖,可以在多個活動或片段之間傳遞數(shù)據(jù)璃弄,檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進行脫機模式,也可以使用許多其他常見應(yīng)用程序遇到的情況构回。
盡管以下建議不是強制性的夏块,但是我們的經(jīng)驗是,遵循這些建議將使您的代碼基礎(chǔ)更加健壯纤掸,可測試和可維護脐供。
您在清單中定義的入口點(活動,服務(wù)借跪,廣播接收器等)不是數(shù)據(jù)的來源政己。相反,他們只應(yīng)該協(xié)調(diào)與該入口點相關(guān)的數(shù)據(jù)子集歇由。由于每個應(yīng)用程序組件的壽命相當(dāng)短沦泌,這取決于用戶與設(shè)備的交互以及運行時的整體當(dāng)前運行狀況赦肃,因此您不希望這些入口點中的任何一個成為數(shù)據(jù)源他宛。
在應(yīng)用程序的各個模塊之間創(chuàng)建明確界定的責(zé)任厅各。例如队塘,不要將從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼跨代碼庫中的多個類或包傳播遮怜。同樣锯梁,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個類中陌凳。
盡可能少地從每個模塊公開合敦。不要試圖創(chuàng)建“only one”的快捷方式充岛,從一個模塊公開內(nèi)部實現(xiàn)細(xì)節(jié)裸准。您可能在短期內(nèi)獲得一些時間炒俱,但隨著您的代碼庫的發(fā)展砸王,您將多次支付技術(shù)債務(wù)谦铃。
在定義模塊之間的交互時驹闰,請考慮如何使每個模塊獨立地進行測試。例如诵肛,如果有一個定義良好的API從網(wǎng)絡(luò)中獲取數(shù)據(jù)褪秀,將會更容易測試將數(shù)據(jù)保存在本地數(shù)據(jù)庫中的模塊媒吗。相反蝴猪,如果將這兩個模塊的邏輯混合在一起,或者在整個代碼庫中撒上網(wǎng)絡(luò)代碼米酬,那么要測試就更加困難了赃额。
你的應(yīng)用程序的核心是什么讓它從其他中脫穎而出跳芳。不要花費時間重復(fù)發(fā)明輪子飞盆,或者一次又一次地寫出相同的樣板代碼孽水。相反女气,將精力集中在讓您的應(yīng)用獨一無二的東西上炼鞠,讓Android架構(gòu)組件和其他推薦的庫處理重復(fù)的樣板。
堅持盡可能多的相關(guān)和新鮮的數(shù)據(jù)软吐,以便您的應(yīng)用程序在設(shè)備處于離線模式時可用姿现。雖然您可以享受持續(xù)高速的連接备典,但用戶可能不會提佣。
您的存儲庫應(yīng)該指定一個數(shù)據(jù)源作為單一的事實來源拌屏。無論何時您的應(yīng)用程序需要訪問這些數(shù)據(jù)倚喂,都應(yīng)始終從單一的事實源頭開始端圈。有關(guān)更多信息,請參閱單一來源的真相仑嗅。
附錄:網(wǎng)絡(luò)狀態(tài)
在上面推薦的應(yīng)用程序體系結(jié)構(gòu)部分,我們故意省略網(wǎng)絡(luò)錯誤和加載狀態(tài)吠冤,以保持樣本簡單拯辙。在本節(jié)中涯保,我們將演示如何使用Resource類公開網(wǎng)絡(luò)狀態(tài)來封裝數(shù)據(jù)及其狀態(tài)夕春。
以下是一個示例實現(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);
}
}
因為從磁盤加載數(shù)據(jù)的時候是一個常見的用例,所以我們要創(chuàng)建一個可以在多個地方重復(fù)使用的幫助類NetworkBoundResource速侈。以下是NetworkBoundResource的決策樹:
它通過觀察資源的數(shù)據(jù)庫開始。當(dāng)?shù)谝淮螐臄?shù)據(jù)庫加載條目時每界,NetworkBoundResource檢查結(jié)果是否足夠好以便分派和/或從網(wǎng)絡(luò)中獲取眨层。請注意谐岁,這兩種情況可能同時發(fā)生,因為您可能希望在從網(wǎng)絡(luò)更新緩存數(shù)據(jù)時顯示緩存的數(shù)據(jù)沛善。
如果網(wǎng)絡(luò)呼叫成功完成金刁,則將響應(yīng)保存到數(shù)據(jù)庫中并重新初始化流媳友。如果網(wǎng)絡(luò)請求失敗醇锚,我們直接發(fā)送失敗焊唬。
注意:在將新數(shù)據(jù)保存到磁盤之后赶促,我們會重新初始化數(shù)據(jù)庫中的數(shù)據(jù)流鸥滨,但通常我們不需要這樣做婿滓,因為數(shù)據(jù)庫將分派更改。另一方面容客,依靠數(shù)據(jù)庫來調(diào)度變化將依賴于不好的副作用但两,因為如果數(shù)據(jù)沒有變化谨湘,數(shù)據(jù)庫可以避免調(diào)度變化紧阔,那么它可能會中斷擅耽。我們也不希望發(fā)送從網(wǎng)絡(luò)到達的結(jié)果乖仇,因為這將違背單一的事實來源(也許在數(shù)據(jù)庫中有觸發(fā)器會改變保存的值)乃沙。如果沒有新的數(shù)據(jù)警儒,我們也不想派遣SUCCESS冷蚂,因為它會向客戶發(fā)送錯誤的信息蝙茶。
以下是NetworkBoundResource類為其子項提供的公共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();
}
請注意钳恕,上面的類定義了兩個類型參數(shù)(ResultType忧额,RequestType)睦番,因為從API返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配托嚣。
另外請注意示启,上面的代碼使用ApiResponse進行網(wǎng)絡(luò)請求夫嗓。 ApiResponse是Retrofit2.Call類的一個簡單的包裝舍咖,將其響應(yīng)轉(zhuǎn)換為LiveData谎仲。
以下是NetworkBoundResource類的其余實現(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將我們的磁盤和網(wǎng)絡(luò)綁定的User實現(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();
}
}