PS: 2018.06.24按照官網(wǎng)最新文檔更新本文翻譯
系列文章導(dǎo)航:
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(一)App架構(gòu)指南
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(二)將Architecture Components引入工程
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(三)處理生命周期
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(四)LiveData
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(五)ViewModel
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(六)Room持久化庫(kù)
原文地址:https://developer.android.com/jetpack/docs/guide
這篇指南適用于熟悉構(gòu)建app基礎(chǔ),并且想要了解構(gòu)建強(qiáng)大的生產(chǎn)級(jí)應(yīng)用的最佳實(shí)踐和推薦架構(gòu)的開發(fā)人員。
App開發(fā)人員面臨的常見問(wèn)題
不像傳統(tǒng)的桌面應(yīng)用,大部分情況下從一個(gè)單一的快捷啟動(dòng)器啟動(dòng),之后作為一個(gè)單一進(jìn)程運(yùn)行,Android應(yīng)用程序結(jié)構(gòu)更復(fù)雜。一個(gè)典型的Android應(yīng)用由多個(gè)應(yīng)用組件構(gòu)建而成,包含activity,fragment,service,content provider以及broadcast receiver。
這些應(yīng)用組件大多數(shù)聲明于app manifest文件中,Android操作系統(tǒng)通過(guò)這個(gè)文件來(lái)決定如何將你的應(yīng)用程序集成到設(shè)備的整體用戶體驗(yàn)中。如前所述,桌面應(yīng)用一般是運(yùn)行在一個(gè)單獨(dú)的進(jìn)程中,而一個(gè)編寫正確的Android應(yīng)用則需要更加靈活。因?yàn)椋脩艨梢圆粩嗲袚Q流程和任務(wù)而任意使用設(shè)備上的不同應(yīng)用程序。
舉個(gè)例子,思考一下當(dāng)你在喜歡的社交應(yīng)用中分享照片時(shí)會(huì)發(fā)生什么。該應(yīng)用觸發(fā)一個(gè)camera intent,Android系統(tǒng)根據(jù)這個(gè)Intent啟動(dòng)相機(jī)應(yīng)用來(lái)處理這個(gè)請(qǐng)求。在這個(gè)時(shí)間點(diǎn),用戶離開了這個(gè)社交應(yīng)用,但是他們的體驗(yàn)卻是無(wú)縫的。接下來(lái),相機(jī)應(yīng)用可能會(huì)觸發(fā)別的Intent,比如啟動(dòng)文件選擇框,這可能會(huì)啟動(dòng)另一個(gè)應(yīng)用。最后用戶返回到社交應(yīng)用,然后分享這張照片。在這個(gè)過(guò)程的任意時(shí)間點(diǎn),用戶也可能會(huì)被一個(gè)電話中斷,在打完電話后才會(huì)回來(lái)分享照片。
在Android中,這種應(yīng)用間跳躍行為是很常見的,因此你的應(yīng)用必須能夠正確得處理這些流程。牢記一點(diǎn),移動(dòng)設(shè)備的資源是有限的,因此在任何時(shí)候,操作系統(tǒng)可能需要?dú)⒌裟承?yīng)用來(lái)為新的應(yīng)用騰空間。
所有這些歸納為一點(diǎn):你的應(yīng)用組件可以被單獨(dú)、無(wú)序地啟動(dòng),并且在任意時(shí)間可以被用戶或者系統(tǒng)銷毀。因?yàn)閼?yīng)用組件生存時(shí)間是短暫的,并且他們的生命周期(創(chuàng)建和銷毀)不受你的控制,所以你不應(yīng)該將任何應(yīng)用程序數(shù)據(jù)或狀態(tài)存儲(chǔ)在應(yīng)用程序組件中,并且應(yīng)用組件不應(yīng)該相互依賴。
常用架構(gòu)準(zhǔn)則
如果不能用應(yīng)用組件來(lái)存儲(chǔ)應(yīng)用數(shù)據(jù)和狀態(tài),那么應(yīng)用應(yīng)該如何架構(gòu)呢?
你應(yīng)該聚焦的最重要的事情,是你應(yīng)用中的關(guān)注點(diǎn)分離(separation of concerns)。一個(gè)常犯的錯(cuò)誤是把所有代碼都寫在Activity或者Fragment中。任何不是用來(lái)處理UI或者操作系統(tǒng)交互相關(guān)的代碼都不應(yīng)該放到這些類中。盡可能讓這些類保持簡(jiǎn)潔能夠讓你避免很多生命周期相關(guān)的問(wèn)題。請(qǐng)記住,你并不擁有這些類,它們僅僅是將你的應(yīng)用和操作系統(tǒng)黏貼在一起的合約類。任何時(shí)候,Android系統(tǒng)可能會(huì)根據(jù)用戶交互或者其他因素(如低內(nèi)存)而銷毀它們。最好盡量減少對(duì)它們的依賴,從而提供一個(gè)堅(jiān)實(shí)的用戶體驗(yàn)。
第二條重要原則是,你應(yīng)該根據(jù)模型來(lái)驅(qū)動(dòng)UI,最好是持久化模型。有兩個(gè)原因來(lái)說(shuō)明持久化是理想的:如果操作系統(tǒng)銷毀了你的應(yīng)用來(lái)釋放資源,你的用戶將不會(huì)丟掉數(shù)據(jù),并且當(dāng)網(wǎng)絡(luò)抖動(dòng)或者沒(méi)有連接時(shí),你的應(yīng)用仍然可以繼續(xù)工作。模型(Model)是那些負(fù)責(zé)處理應(yīng)用數(shù)據(jù)的組件。它們獨(dú)立于View(視圖)和應(yīng)用組件,因此它們與這些組件的生命周期問(wèn)題隔離。保持UI代碼簡(jiǎn)單,遠(yuǎn)離應(yīng)用邏輯將會(huì)更容易管理。將你的應(yīng)用程序構(gòu)建在那些數(shù)據(jù)管理責(zé)任定義良好的模型類之上,將會(huì)使它們可測(cè)試,并具備應(yīng)用一致性。
推薦的應(yīng)用架構(gòu)
在這一節(jié),我們通過(guò)一個(gè)用例來(lái)演示如何使用Architecture Components架構(gòu)應(yīng)用程序。
注意:沒(méi)有哪一種編寫app的方式能夠最佳滿足所有場(chǎng)景。話雖如此,這個(gè)推薦的架構(gòu)對(duì)于大多數(shù)場(chǎng)景都是一個(gè)好的開端。如果你已經(jīng)擁有一種好的編寫Android應(yīng)用的方式,你可以不需要改變。
假設(shè)我們?cè)跇?gòu)建一個(gè)展示用戶信息的UI。用戶信息可以使用REST API從我們的私有后端獲取到。
構(gòu)建界面
UI包括一個(gè)fragment UserProfileFragment.java,以及相應(yīng)的布局文件user_profile_layout.xml。
為了驅(qū)動(dòng)UI,我們的數(shù)據(jù)模型需要持有兩個(gè)數(shù)據(jù)元素。
-
User ID: 用戶ID。最好通過(guò)fragment參數(shù)來(lái)傳遞這個(gè)數(shù)據(jù)。如果系統(tǒng)銷毀了進(jìn)程,該信息會(huì)被保留,當(dāng)app重啟后再次可用 -
User Object:持有用戶數(shù)據(jù)的一個(gè)POJO對(duì)象。
我們創(chuàng)建一個(gè)繼承自ViewModel的UserProfileViewModel類,該類將持有上面的信息。
ViewModel向具體的UI組件(比如
fragment和activity)提供數(shù)據(jù),并且處理與數(shù)據(jù)處理業(yè)務(wù)部分的通信,例如調(diào)用別的組件加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改。ViewModel并不知道View,并且不受配置改變的影響,比如因?yàn)樾D(zhuǎn)而重新創(chuàng)建Activity。
現(xiàn)在,我們有3個(gè)文件:
-
user_profile.xml: 定義UI界面 -
UserProfileViewModel.java: 為UI提供數(shù)據(jù)的類 -
UserProfileFragment.java: UI控制器。展示ViewModel的數(shù)據(jù),響應(yīng)用戶交互。
下面是我們的初始實(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)在,我們有了這三個(gè)代碼模塊,怎么將他們連接起來(lái)呢?畢竟,當(dāng)ViewModel的user字段設(shè)置以后,我們需要一種方式來(lái)通知UI。所以,該LiveData類登場(chǎng)了。
LiveData是一個(gè)可觀察的數(shù)據(jù)持有者(data holder)。它允許應(yīng)用組件觀察LiveData對(duì)象的改變,而不需要在它們之間創(chuàng)建顯式和剛性的依賴路徑。LiveData還尊重應(yīng)用程序組件(Activity,Fragment,Service)的生命周期狀態(tài),并且做正確的事情以防止對(duì)象泄漏, 從而使你的應(yīng)用程序不消耗更多的內(nèi)存。
注意:如果你已經(jīng)在使用像RxJava或Agrea這樣的庫(kù),你可以繼續(xù)使用而不必替換為
LiveData。但是當(dāng)你在使用這些庫(kù)或其他方法的時(shí)候,請(qǐng)確保能正確處理生命周期,比如說(shuō)當(dāng)相關(guān)的LifecycleOwner停止時(shí)候你的數(shù)據(jù)流應(yīng)該暫停,而LifecycleOwner銷毀的時(shí)候,你的數(shù)據(jù)流也應(yīng)該被銷毀。你也可以添加android.arch.lifecycle:reactivestreams庫(kù),將LiveData和另一個(gè)響應(yīng)式流庫(kù)配合使用(比如RxJava2)。
現(xiàn)在我們將UserProfileViewModel中的User域替換成LiveData<User>,這樣當(dāng)數(shù)據(jù)更新的時(shí)候,Fragment可以得到通知。LiveData最炫酷的功能是,它是生命周期感知的,可以自動(dòng)清理那些不再會(huì)使用到的引用。
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
現(xiàn)在我們修改UserProfileFragment來(lái)觀察數(shù)據(jù)并且更新UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
每當(dāng)數(shù)據(jù)改變時(shí),onChanged回調(diào)將會(huì)執(zhí)行,然后UI將會(huì)刷新。
如果你熟悉別的使用可觀察回調(diào)的庫(kù),則可能已經(jīng)意識(shí)到我們不必重寫Fragment的onStop()方法來(lái)停止觀察數(shù)據(jù)。使用LiveData是不必這么做的,因?yàn)樗巧芷诟兄?,也就是說(shuō)只有Fragment在激活狀態(tài)(收到onStart(),但是沒(méi)有收到onStop())的時(shí)候,LiveData才會(huì)調(diào)用回調(diào)。當(dāng)Fragment收到onDestroy()時(shí),LiveData會(huì)自動(dòng)移除觀察者。
我們也不需要做任何特殊的事情來(lái)處理配置改變(比如,用戶旋轉(zhuǎn)屏幕)。當(dāng)配置改變時(shí),一旦新的Fragment創(chuàng)建,ViewModel會(huì)自動(dòng)還原,它將收到同一個(gè)ViewModel實(shí)例,并且將立即使用當(dāng)前的數(shù)據(jù)調(diào)用回調(diào)。這也是為啥 ViewModels不應(yīng)該直接引用Views。它們可以超越View的生命周期。參見ViewModel的
拉取數(shù)據(jù)
現(xiàn)在我們已經(jīng)將ViewModel連接到Fragment,但是ViewModel如何拉取用戶數(shù)據(jù)呢 ?在這個(gè)例子里,我們假設(shè)后端提供REST API。我們將采用Retrofit庫(kù)訪問(wèn)后端,當(dāng)然你也可以用其他庫(kù)達(dá)到同樣的目的。
下面是我們用來(lái)與后端通信的retrofit WebService:
public interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
ViewModel的一個(gè)天真版實(shí)現(xiàn),可以直接調(diào)用WebService拉取數(shù)據(jù),然后將其賦值給user對(duì)象。盡管這個(gè)可用,但隨著你應(yīng)用的迭代后續(xù)將很難維護(hù)。它賦予了ViewModel類太多責(zé)任,這違反了我們之前提到的關(guān)注點(diǎn)分離原則。此外,ViewModel的范圍被綁到了Activity或Fragment的生命周期上,因此在生命周期結(jié)束時(shí)將會(huì)丟失所有的數(shù)據(jù),這是一個(gè)很糟糕的用戶體驗(yàn)。相反,我們的ViewModel將把這項(xiàng)工作委托給一個(gè)新的Repository模塊。
Repository模塊負(fù)責(zé)處理數(shù)據(jù)操作。他們向app的其他部分提供一個(gè)干凈整潔的API。他們知道從哪里去獲取數(shù)據(jù),知道當(dāng)數(shù)據(jù)更新時(shí)候需要調(diào)用哪些API。你可以把他們看作不同數(shù)據(jù)源(持久化數(shù)據(jù),Web服務(wù),緩存等)之間的中間人。
下面的UserRepository類使用WebService來(lái)拉取用戶數(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;
}
}
盡管這個(gè)repository模塊看起來(lái)不需要,但它起著很重要的作用。它向應(yīng)用的其余部分抽象了數(shù)據(jù)源?,F(xiàn)在我們的ViewModel不知道我們的數(shù)據(jù)是由Webservice拉取的,這也就意味著如果有需要的話我們可以將它替換成其他的實(shí)現(xiàn)。
注意:為了簡(jiǎn)單起見,我們?nèi)サ袅司W(wǎng)絡(luò)異常情況。有關(guān)暴露錯(cuò)誤和加載狀態(tài)的替代實(shí)現(xiàn)版本,請(qǐng)參閱附錄:暴露網(wǎng)絡(luò)狀態(tài)
管理組件間依賴關(guān)系
上面的UserRepository類需要一個(gè)Webservice實(shí)例來(lái)完成它的工作。它可以簡(jiǎn)單創(chuàng)建一個(gè)實(shí)例,但是這樣做它需要知道構(gòu)建Webservice的依賴關(guān)系。這將會(huì)使代碼復(fù)雜化并且重復(fù)(比如,每個(gè)需要一個(gè)Webservice實(shí)例的類都需要知道如何使用它的依賴來(lái)構(gòu)造它)。此外,UserRepository可能不是唯一一個(gè)需要Webservice的類。如果每個(gè)類都創(chuàng)建一個(gè)新的WebService,這將會(huì)是資源浪費(fèi)。
有兩種模式可以用來(lái)解決這個(gè)問(wèn)題:
- 依賴注入:依賴注入允許類定義他們的依賴關(guān)系而不創(chuàng)建他們。在運(yùn)行時(shí),另外的類負(fù)責(zé)提供這些依賴。我們推薦Google的Dagger2庫(kù)來(lái)實(shí)現(xiàn)Android應(yīng)用的依賴注入。
-
服務(wù)定位器(Service Locator):服務(wù)定位器提供了一個(gè)注冊(cè)表,其中類可以獲取它們的依賴關(guān)系,而不是構(gòu)造它們。與依賴注入相比,實(shí)施起來(lái)相對(duì)容易,因此如果你不熟悉依賴注入,請(qǐng)改用
Service Locator。
這些模式允許你擴(kuò)展代碼,因?yàn)樗鼈優(yōu)楣芾硪蕾囮P(guān)系提供了明確的模式,而會(huì)重復(fù)代碼或增加復(fù)雜性。這兩種方式都允許交換實(shí)現(xiàn)方式進(jìn)行測(cè)試,這也是采用它們的主要好處之一。
在這個(gè)例子里,我們將使用Dagger2來(lái)管理依賴。
連接ViewModel和repository
現(xiàn)在我們修改我們的UserProfileViewModel來(lái)使用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ì)于抽象Web服務(wù)的調(diào)用是很好的,但是由于它僅僅依賴于一個(gè)數(shù)據(jù)源,它并不是非常有用。
上面的UserRepository實(shí)現(xiàn)的問(wèn)題是,在拉取數(shù)據(jù)后,它并沒(méi)有保存在任何地方。如果用戶離開了UserProfileFragment 并返回,app將重新拉取數(shù)據(jù)。這個(gè)很糟糕,因?yàn)閮蓚€(gè)原因:它浪費(fèi)了寶貴的網(wǎng)絡(luò)帶寬,并迫使用戶等待新的查詢完成。為了解決這個(gè)問(wèn)題,我們將向我們的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ù)
在我們現(xiàn)在的實(shí)現(xiàn)中,如果用戶旋轉(zhuǎn)屏幕或者離開后返回到應(yīng)用程序,現(xiàn)有UI將立即可見,因?yàn)?code>repository可以從內(nèi)存緩存中拉取數(shù)據(jù)。但是如果用戶離開應(yīng)用并在Android系統(tǒng)殺掉進(jìn)程幾個(gè)小時(shí)候后 重新啟動(dòng),會(huì)發(fā)生什么?
按現(xiàn)在的實(shí)現(xiàn),我們需要從網(wǎng)絡(luò)中再次拉取數(shù)據(jù)。這不僅是一個(gè)糟糕的用戶體驗(yàn),也是非常浪費(fèi)的,因?yàn)樗鼘⑹褂靡苿?dòng)數(shù)據(jù)來(lái)重新拉取相同的數(shù)據(jù)。你可以通過(guò)緩存Web請(qǐng)求來(lái)簡(jiǎn)單解決這個(gè)問(wèn)題,但它將會(huì)產(chǎn)生新的問(wèn)題。如果相同的用戶數(shù)據(jù)從另一種類型的請(qǐng)求(例如,獲取一個(gè)朋友列表)出現(xiàn),會(huì)發(fā)生什么情況?那么,你的應(yīng)用程序可能會(huì)顯示不一致數(shù)據(jù),這是最令人困惑的用戶體驗(yàn)。舉個(gè)栗子,相同的用戶數(shù)據(jù)可能展現(xiàn)不同因?yàn)榕笥蚜斜碚?qǐng)求和用戶請(qǐng)求可以在不同的時(shí)間執(zhí)行。你的應(yīng)用程序需要合并他們來(lái)避免展示不一致的數(shù)據(jù)。
處理這種問(wèn)題的正確辦法是使用一個(gè)持久化模型。這就是Room持久化庫(kù)來(lái)拯救的地方!
Room是一個(gè)對(duì)象映射庫(kù),可以使用最少的樣板代碼提供本地?cái)?shù)據(jù)持久性。在編譯時(shí),它根據(jù)模式驗(yàn)證每個(gè)查詢,這樣損壞的SQL查詢導(dǎo)致編譯時(shí)錯(cuò)誤而不是運(yùn)行時(shí)故障。Room抽象了使用原始SQL表和查詢的基本實(shí)現(xiàn)細(xì)節(jié)。它還允許觀察對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢)的改變,通過(guò)LiveData對(duì)象暴露這些更改。另外,它明確定義了解決常見問(wèn)題的線程約束,例如在主線程上訪問(wèn)存儲(chǔ)。
注意:如果你熟悉其他持久化方案像
SQLite ORM或其他的數(shù)據(jù)庫(kù)比如Realm,則無(wú)需將其替換為Room,除非Room的功能集與你的用例更相關(guān)。如果你在編寫新的app或者重構(gòu)老的app,我們建議使用Room來(lái)做數(shù)據(jù)持久化。
要使用Room,我們需要定義我們的本地協(xié)議(schema)。首先,在User類上增加@Entity注解,將其標(biāo)識(shí)為數(shù)據(jù)庫(kù)的一個(gè)表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,通過(guò)繼承RoomDatabase來(lái)創(chuàng)建你的應(yīng)用數(shù)據(jù)庫(kù)類。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意到MyDatabase是抽象的。Room將自動(dòng)提供它的實(shí)現(xiàn)。詳細(xì)內(nèi)容請(qǐng)參閱Room文檔。
現(xiàn)在,我們需要一種方式把user數(shù)據(jù)插入到數(shù)據(jù)庫(kù)中。為了滿足這個(gè),我們創(chuàng)建一個(gè)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ù)類中引用這個(gè)DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
注意load()方法返回一個(gè)LiveData<User>。Room知道數(shù)據(jù)庫(kù)什么時(shí)候被修改,并且當(dāng)數(shù)據(jù)變化的時(shí)候,它會(huì)自動(dòng)通知所有處于激活狀態(tài)的觀察者。因?yàn)樗褂玫氖?code>LiveData,這將是很高效的,因?yàn)樗粫?huì)在至少有一個(gè)激活觀察者時(shí)更新數(shù)據(jù)。
注意:
Room基于數(shù)據(jù)庫(kù)表修改來(lái)檢查無(wú)效修改,這意味著它可能會(huì)發(fā)送假的正面通知。( Room checks invalidations based on table modifications which means it may dispatch false positive notifications.)
現(xiàn)在我們可以修改我們的 UserRepository來(lái)合并Room數(shù)據(jù)源。
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData<User> getUser(String userId) {
refreshUser(userId);
// 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ù)來(lái)源,我們也不需要改變我們的 UserProfileViewModel或者UserProfileFragment。這就是抽象提供的靈活性。這對(duì)于測(cè)試來(lái)說(shuō)也是極好的,因?yàn)楫?dāng)測(cè)試你的UserProfileViewModel的時(shí)候,你可以提供一個(gè)假的UserRepository。
現(xiàn)在我們的代碼是完整的。如果用戶幾天后返回到同一個(gè)頁(yè)面,他們可以立即看到用戶信息因?yàn)槲覀儗⑦@部分?jǐn)?shù)據(jù)持久化保存了。同時(shí),如果數(shù)據(jù)過(guò)時(shí),我們的reposity將在后臺(tái)更新數(shù)據(jù)。當(dāng)然,這取決于你的使用場(chǎng)景,如果持久化數(shù)據(jù)太舊,你可能不希望對(duì)其展示。
在某些使用場(chǎng)景,比如下拉刷新,當(dāng)進(jìn)行網(wǎng)絡(luò)操作時(shí)候,對(duì)于UI來(lái)說(shuō)向用戶展示操作進(jìn)度是非常重要的。把UI操作與實(shí)際數(shù)據(jù)分離是很好的做法,因?yàn)閿?shù)據(jù)可能可能由于各種原因被更新(例如,如果我們獲取一個(gè)好友列表,則可能再次獲取相同的用戶,觸發(fā)LiveData<User>更新)。從UI的角度來(lái)說(shuō),一個(gè)正在進(jìn)行中的請(qǐng)求只是一個(gè)數(shù)據(jù)點(diǎn)這樣一個(gè)事實(shí),和別的數(shù)據(jù)片(比如User對(duì)象)沒(méi)啥區(qū)別。
對(duì)于這種場(chǎng)景,有兩種常見的解決辦法:
- 更改
getUser返回一個(gè)包含網(wǎng)絡(luò)操作異常的LiveData。附錄:暴露網(wǎng)絡(luò)狀態(tài)一節(jié)提供了一個(gè)實(shí)現(xiàn)例子。 - 在
reposity類中提供一個(gè)可以返回User刷新狀態(tài)的接口。如果要僅在UI中顯示網(wǎng)絡(luò)狀態(tài)(為了響應(yīng)用戶操作,比如下拉刷新),這個(gè)選項(xiàng)會(huì)更好。
真相的唯一來(lái)源
不同的REST API端返回相同的數(shù)據(jù),這種情況是很常見的。舉個(gè)栗子,如果我們的后臺(tái)有一個(gè)端點(diǎn)返回一個(gè)朋友列表,那么同一個(gè)user對(duì)象可能來(lái)自不同的API端點(diǎn),也可能是不同的粒度。如果UserRepository按原樣從WebService請(qǐng)求然后返回響應(yīng),那么我們的UI可能會(huì)顯示不一致的數(shù)據(jù),因?yàn)閿?shù)據(jù)可能會(huì)在這些請(qǐng)求之間的服務(wù)端發(fā)生更改。這也是為什么在UserRepository的實(shí)現(xiàn)里,Web服務(wù)回調(diào)僅僅將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)中。然后,對(duì)于數(shù)據(jù)庫(kù)的改變會(huì)觸發(fā)激活的LiveData的回調(diào)。
在這個(gè)模型里面,數(shù)據(jù)庫(kù)扮演著真相的唯一來(lái)源角色,app的其他部分通過(guò)reposity來(lái)訪問(wèn)它。不管你是否使用磁盤緩存,我們推薦你的reposity指定某個(gè)數(shù)據(jù)源作為應(yīng)用程序的其余部分的唯一真實(shí)來(lái)源。
測(cè)試
我們已經(jīng)提到分離的好處之一是可測(cè)試性。讓我們看看如何測(cè)試每個(gè)代碼模塊。
UI和交互:這將是你唯一需要Android UI Instrumentation 測(cè)試的時(shí)間。測(cè)試UI代碼的最佳途徑是創(chuàng)建一個(gè)Espresso測(cè)試。你可以創(chuàng)建一個(gè)fragment,然后給它提供一個(gè)Mock的ViewModel。因?yàn)檫@個(gè)fragment僅僅與ViewModel通信,對(duì)其進(jìn)行Mock將足以完全測(cè)試這個(gè)UI。
ViewModel:ViewModel可以通過(guò)JUnit來(lái)測(cè)試。你只需要Mock UserRepository就可以測(cè)試它。
UserRepository: 你也可以用JUnit來(lái)測(cè)試UserRepository。你需要mock Webservice 和DAO。你可以測(cè)試它是否執(zhí)行了正確的Web服務(wù)調(diào)用,將結(jié)果保存到數(shù)據(jù)庫(kù)中,如果數(shù)據(jù)已經(jīng)被緩存和更新,則不會(huì)發(fā)生任何不必要的請(qǐng)求。
-
UserDao:測(cè)試DAO類的推薦方法是使用instrumentation 測(cè)試。因?yàn)檫@些instrumentation 測(cè)試不需要任何UI,他們將運(yùn)行很快。對(duì)于每個(gè)測(cè)試,你可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù)來(lái)保證測(cè)試沒(méi)有任何副作用(如更改磁盤上的數(shù)據(jù)庫(kù)文件)
Room還允許指定數(shù)據(jù)庫(kù)實(shí)現(xiàn),因此你可以通過(guò)提供SupportSQLiteOpenHelper的JUnit實(shí)現(xiàn)來(lái)測(cè)試它。通常不推薦使用此方法,因?yàn)檫\(yùn)行在設(shè)備上的SQLite版本可能與你主機(jī)上的SQLite版本不同。 WebService:使測(cè)試獨(dú)立于外部世界是很重要的,甚至你的WebService測(cè)試應(yīng)該避免執(zhí)行對(duì)后臺(tái)的網(wǎng)絡(luò)請(qǐng)求。有很多庫(kù)可以解決這個(gè)問(wèn)題。例如,
MockWebServer是一個(gè)很好的庫(kù),它可以幫助你創(chuàng)建一個(gè)假的本地服務(wù)器用于測(cè)試。-
測(cè)試
Artifact:Architecture Components提供一個(gè)maven artifact 來(lái)控制它的后臺(tái)線程。在android.arch.core:core-testing artifact里面,有兩個(gè)JUnit規(guī)則:- InstantTaskExecutorRule:該規(guī)則可用于強(qiáng)制
Architecture Components在調(diào)用線程上立即執(zhí)行任何后臺(tái)操作。 - CountingTaskExecutorRule:該規(guī)則可用于instrumentation 測(cè)試,以等待
Architecture Components的后臺(tái)操作或連接到Espresso作為閑置資源。
- InstantTaskExecutorRule:該規(guī)則可用于強(qiáng)制
最終架構(gòu)
下面的圖顯示了我們推薦架構(gòu)的所有模塊,以及它們之間如何交互。

指導(dǎo)原則
編程是一個(gè)創(chuàng)意領(lǐng)域,構(gòu)建Android應(yīng)用程序也不例外。有很多辦法去解決一個(gè)問(wèn)題,無(wú)論是在多個(gè)Activity或Fragment之間傳遞數(shù)據(jù),檢索遠(yuǎn)程數(shù)據(jù)并將其保存到本地用于離線模式,還是任何其他常用應(yīng)用遇到的常見場(chǎng)景。
雖然以下建議不是強(qiáng)制性的,但我們的經(jīng)驗(yàn)是,從長(zhǎng)遠(yuǎn)看來(lái)遵循這些建議將使你的代碼庫(kù)更強(qiáng)健,可測(cè)試和可維護(hù)。
- 你定義在manifest文件中的入口點(diǎn)——activity,service,broadcast recevier等等,不是數(shù)據(jù)源。相反,它們應(yīng)該只是與該入口點(diǎn)相關(guān)的數(shù)據(jù)自己的協(xié)調(diào)者。因?yàn)槊總€(gè)應(yīng)用組件的壽命相當(dāng)短,取決于用戶與設(shè)備的交互以及運(yùn)行時(shí)的整體狀況,你不希望任何的這些入口點(diǎn)變成數(shù)據(jù)源。
- 堅(jiān)決在你的應(yīng)用程序各個(gè)模塊之間創(chuàng)建明確定義的責(zé)任邊界。比如說(shuō),不要將從網(wǎng)絡(luò)中加載數(shù)據(jù)的代碼散布到各個(gè)類或者包中。同樣,不要將無(wú)關(guān)責(zé)任比如數(shù)據(jù)緩存和數(shù)據(jù)綁定雜糅到同一個(gè)類中。
- 每個(gè)模塊盡可能少的向外暴露。不要試圖創(chuàng)建一個(gè)從一個(gè)模塊暴露內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的萬(wàn)能快捷方式。你可能會(huì)在短期內(nèi)節(jié)約一點(diǎn)時(shí)間,但隨著代碼庫(kù)的發(fā)展,你將多付出很多技術(shù)債務(wù)。
- 當(dāng)你定義模塊之間的交互時(shí),請(qǐng)考慮如何讓每個(gè)模塊分離成可測(cè)試的。例如,擁有一個(gè)定義良好的從網(wǎng)絡(luò)獲取數(shù)據(jù)的API將使得更容易測(cè)試在本地?cái)?shù)據(jù)庫(kù)中持久化該數(shù)據(jù)的模塊。相反,如果將這兩個(gè)模塊的邏輯雜糅到一起,或者將網(wǎng)絡(luò)代碼散布在你整個(gè)代碼庫(kù)中,將會(huì)非常難以測(cè)試。
- 你的應(yīng)用程序的核心是能夠讓它脫穎而出的那部分東西。不要花時(shí)間重復(fù)造輪子,或者一遍遍寫模板代碼。相反,你應(yīng)該將精力集中到讓你的應(yīng)用獨(dú)一無(wú)二,處理重復(fù)模板代碼的事情就交給Android Architecture Component和其他推薦的庫(kù)吧。
- 相對(duì)多一點(diǎn)持久化數(shù)據(jù),盡可能更新數(shù)據(jù),以便當(dāng)設(shè)備處于離線狀態(tài)時(shí),你的應(yīng)用仍然可用。雖然你可能享受穩(wěn)定和高速的連接,但是你的用戶可能不會(huì)。
- 你的repository 應(yīng)該指定一個(gè)數(shù)據(jù)源作為唯一真相數(shù)據(jù)源。每當(dāng)你的應(yīng)用程序需要訪問(wèn)數(shù)據(jù)片時(shí),它應(yīng)該始終源自這個(gè)唯一的真相數(shù)據(jù)源。有關(guān)更多信息,請(qǐng)參考唯一真相源。
附錄:暴露網(wǎng)絡(luò)狀態(tài)
在上面推薦的應(yīng)用架構(gòu)這一節(jié),我們有意省略了網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)來(lái)讓樣例代碼簡(jiǎn)單。在這一節(jié),我們演示一種使用Resource類封裝數(shù)據(jù)和狀態(tài)的暴露網(wǎng)絡(luò)狀態(tài)的方法。
下面是樣例實(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)闊o(wú)論是從磁盤還是網(wǎng)絡(luò)加載數(shù)據(jù)都是一個(gè)常見的使用場(chǎng)景,我們將創(chuàng)建一個(gè)可以在多個(gè)地方重復(fù)使用的幫助類NetworkBoundResource。下面是NetworkBoundResource的決策樹:

它從觀察資源的數(shù)據(jù)庫(kù)開始。當(dāng)數(shù)據(jù)條目第一次從數(shù)據(jù)庫(kù)加載的時(shí)候,NetworkBoundResource檢查結(jié)果足夠好以便被分派,或者應(yīng)該從網(wǎng)絡(luò)中獲取。請(qǐng)注意,這兩個(gè)可能同時(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ù)保存到磁盤后,我們從數(shù)據(jù)庫(kù)重新初始化流,盡管通常我們不需要那么做,因?yàn)閿?shù)據(jù)庫(kù)會(huì)分派變化。另一方面,依賴數(shù)據(jù)庫(kù)分派變化將依賴于不利的副作用,因?yàn)槿绻麛?shù)據(jù)沒(méi)有變化,數(shù)據(jù)庫(kù)可以避免分發(fā)變化。我們也不想分派從網(wǎng)絡(luò)達(dá)到的結(jié)果,因?yàn)檫@將違反唯一真相來(lái)源(也許在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變打算保存的值)。我們也不想在沒(méi)有新數(shù)據(jù)的情況下發(fā)送success,因?yàn)樗赡軙?huì)向客戶端發(fā)送錯(cuò)誤的信息。
下面是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
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
請(qǐng)注意,上面的類定義了兩個(gè)類型參數(shù)(ResultType, RequestType),因?yàn)锳PI返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配。
也請(qǐng)注意,上面的代碼使用ApiResponse做網(wǎng)絡(luò)請(qǐng)求。ApiResponse是Retrofit2.Call類的的簡(jiǎn)單包裝,用于將其響應(yīng)轉(zhuǎn)換為LiveData。
下面是NetworkBoundResource類的剩余實(shí)現(xiàn):
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// 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();
}
}
現(xiàn)在,我們可以使用NetworkBoundResource將我們的磁盤和網(wǎng)絡(luò)綁定User實(shí)現(xiàn)寫入到repository。
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();
}
}