官方文檔鏈接:https://developer.android.google.cn/topic/libraries/architecture/guide.html
1.前言
由于用戶(hù)在使用某一功能時(shí)會(huì)涉及不同的應(yīng)用程序苗胀,需要不斷地切換流程和任務(wù)衙猪。舉個(gè)例子,在使用社交軟件分享照片時(shí)匈棘,會(huì)需要打開(kāi)攝像頭精置,也會(huì)需要從圖庫(kù)中選擇文件谭胚,而將數(shù)據(jù)返回社交軟件的過(guò)程中常拓,有可能會(huì)有電話打來(lái)掐禁。這一下子啟動(dòng)了許多應(yīng)用程序怜械,但是移動(dòng)設(shè)備資源有限,操作系統(tǒng)隨時(shí)會(huì)殺死一些應(yīng)用程序來(lái)騰出空間給新的應(yīng)用傅事。所以應(yīng)用程序組件的存在不由開(kāi)發(fā)者控制缕允,不應(yīng)該存儲(chǔ)數(shù)據(jù)或狀態(tài),更不應(yīng)該彼此依賴(lài)蹭越。
之前通過(guò)生命周期進(jìn)行相關(guān)操作障本,但這引入另一套邏輯,增加代碼的復(fù)雜度响鹃。
2.常見(jiàn)架構(gòu)原則
2.1.View層分離
使Activity和Fragment中的代碼盡可能的少驾霜,只處理與界面或與操作系統(tǒng)的交互。因?yàn)檫@些類(lèi)是操作系統(tǒng)和應(yīng)用程序的中間件买置,不由開(kāi)發(fā)者控制粪糙,所以應(yīng)該最小化依賴(lài)。
2.2.Model層驅(qū)動(dòng)
通過(guò)持久化模型驅(qū)動(dòng)界面忿项,具有兩點(diǎn)好處:
- 當(dāng)操作系統(tǒng)釋放組件資源時(shí)蓉冈,用戶(hù)數(shù)據(jù)也不會(huì)丟失城舞;
- 當(dāng)網(wǎng)絡(luò)連接狀態(tài)不好時(shí),應(yīng)用程序也能正常工作寞酿。
持久化模型獨(dú)立于組件家夺,不受系統(tǒng)的控制》サ基于它的應(yīng)用程序可以保證界面代碼簡(jiǎn)單秦踪,同時(shí)業(yè)務(wù)邏輯具有可測(cè)試性。
3.推薦的應(yīng)用框架
沒(méi)有一種架構(gòu)適用于所有場(chǎng)景掸茅。意味著椅邓,這是一個(gè)好的起點(diǎn),但若已經(jīng)有更適合的了昧狮,是不需要改變的景馁。以網(wǎng)絡(luò)獲取并顯示用戶(hù)配置信息為例子,展示架構(gòu)組件的使用方式逗鸣。
3.1.建立用戶(hù)界面
界面包含一個(gè)UserProfileFragment.java
的組件類(lèi)和user_profile_layout.xml
的布局文件合住。為了驅(qū)動(dòng)界面,基于ViewModel類(lèi)創(chuàng)建UserProfileViewModel.java
的數(shù)據(jù)模型來(lái)保存信息撒璧。數(shù)據(jù)元素主要包括:
- The User ID:用戶(hù)標(biāo)識(shí)符透葛。最好通過(guò)
Fragment Arguments
傳入Fragment,那樣操作系統(tǒng)銷(xiāo)毀應(yīng)用進(jìn)程時(shí)卿樱,id將被保存以便重啟時(shí)使用僚害。 - The User object:一個(gè)POJO類(lèi)保存用戶(hù)數(shù)據(jù)。
一個(gè)ViewModel給一個(gè)特定的界面組件(Activity或Fragment)提供數(shù)據(jù)繁调,并處理業(yè)務(wù)邏輯中數(shù)據(jù)相關(guān)的操作萨蚕,如調(diào)用其它組件來(lái)加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶(hù)的修改。ViewModel與View隔離蹄胰,不受屏幕旋轉(zhuǎn)導(dǎo)致重建等Configuration Changes
的影響岳遥。而Activity或Fragment則是UI Controller,與用戶(hù)行為和ViewModel進(jì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 LifecycleFragment {
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);
}
}
目前使用LifecycleFragment代替Fragment浩蓉,等到lifecycles版本穩(wěn)定后,支持庫(kù)中的Fragment將實(shí)現(xiàn)LifecycleOwner接口宾袜。
ViewModel與Presenter最大的不同是捻艳,Presenter內(nèi)部封裝著一系列的行為,而ViewModel持有的是數(shù)據(jù)(狀態(tài))试和,那么數(shù)據(jù)如何傳遞呢讯泣?這里就得提LiveData了。
LiveData是一個(gè)可被觀察的數(shù)據(jù)持有者阅悍。它讓?xiě)?yīng)用中的組件觀察自己的變化好渠,卻不需要顯式的和剛性的依賴(lài)昨稼。LiveData同時(shí)會(huì)監(jiān)聽(tīng)?wèi)?yīng)用組件(Activity,F(xiàn)ragment拳锚,Services)的生命周期狀態(tài)假栓,并且做正確的事情來(lái)防止對(duì)象的內(nèi)存泄露。
如果已經(jīng)使用RxJava或Agera庫(kù)霍掺,你可以繼續(xù)使用來(lái)替代LiveData匾荆。不過(guò)當(dāng)你使用它們時(shí),確保正確處理生命周期杆烁,例如:LifecycleOwner調(diào)用了
onStop()
方法牙丽,需暫停相關(guān)的數(shù)據(jù)流;LifecycleOwner調(diào)用了onDestory()
方法兔魂,需銷(xiāo)毀相關(guān)的數(shù)據(jù)流烤芦。你也可以添加android.arch.lifecycle:reactivestreams
工件來(lái)讓LiveData和其它的響應(yīng)流庫(kù)(RxJava2)一起使用。
現(xiàn)在析校,用LiveData<User>
來(lái)替代User
构罗,這樣當(dāng)數(shù)據(jù)改變時(shí),F(xiàn)ragment將會(huì)收到通知智玻。更好的是遂唧,LiveData是支持生命周期的,當(dāng)自己不再被使用時(shí)吊奢,自動(dòng)清理引用盖彭。
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
// UserProfileFragment.java
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
...
viewModel.getUser().observe(this, user -> {
// update UI
});
}
與其它使用觀察回調(diào)的庫(kù)不同。當(dāng)Fragment不處于活躍狀態(tài)時(shí)事甜,是不會(huì)回調(diào)的谬泌,所以不需要手動(dòng)在Fragment的onStop()
方法中停止觀察數(shù)據(jù);當(dāng)Fragment銷(xiāo)毀了逻谦,LiveData將會(huì)移除觀察者,釋放資源陪蜻。
不需要做額外的事邦马,ViewModel在配置改變時(shí),將自動(dòng)恢復(fù)相同的ViewModel實(shí)例及對(duì)當(dāng)前數(shù)據(jù)的回調(diào)宴卖。
3.2.獲取數(shù)據(jù)
將ViewModel和Fragment連接后滋将,還需要ViewModel獲取到用戶(hù)數(shù)據(jù)。這里症昏,假設(shè)使用Retrofit庫(kù)來(lái)訪問(wèn)后端接口随闽。
public interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
雖然可以在ViewModel中直接獲取數(shù)據(jù)并復(fù)制給User對(duì)象,但隨著項(xiàng)目的變大肝谭,將越來(lái)越難以維護(hù)掘宪,而且給ViewModel太多的職責(zé)蛾扇,與單一職責(zé)的原則相違背。此外魏滚,ViewModel的活動(dòng)范圍與Activity或Fragment的生命周期相關(guān)镀首,即生命周期結(jié)束后將丟失所有數(shù)據(jù)。為此鼠次,引入了Repository這個(gè)概念更哄。
Repository是專(zhuān)門(mén)用來(lái)處理數(shù)據(jù)操作的。知道從什么地方獲取數(shù)據(jù)和調(diào)用什么接口更新數(shù)據(jù)腥寇,是不同數(shù)據(jù)源之間的中轉(zhuǎn)成翩,例如持久化模型、Web服務(wù)赦役、緩存等捕传。
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;
}
}
這樣將數(shù)據(jù)源從應(yīng)用其它部分獨(dú)立出來(lái),ViewModel不知道與什么數(shù)據(jù)源交互扩劝,便于替換庸论。但是UserRepository需要Webservice實(shí)例,若通過(guò)構(gòu)造方法提供棒呛,每個(gè)使用Webservice的類(lèi)都得知道它的構(gòu)造方法聂示,使依賴(lài)變得復(fù)雜,同時(shí)都創(chuàng)建對(duì)象將占用大量資源簇秒。這里提供兩種方法參考:
- 依賴(lài)注入:允許類(lèi)定義它們之間的依賴(lài)而不需要立馬構(gòu)建出來(lái)鱼喉,等到運(yùn)行時(shí),由其它的類(lèi)提供這些依賴(lài)趋观。
- 服務(wù)定位:提供一個(gè)倉(cāng)庫(kù)扛禽,類(lèi)可以從中獲取它們所需的依賴(lài)而不是創(chuàng)建它們。相較于依賴(lài)注入皱坛,它更容易實(shí)現(xiàn)编曼。
這些模式可以方便地?cái)U(kuò)展代碼,因?yàn)樘峁┝饲逦姆绞焦芾硪蕾?lài)剩辟,而不需要增加其它代碼掐场。更重要的是它們都很容易測(cè)試。
3.3.ViewModel和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;
}
}
3.4.緩存數(shù)據(jù)
若Repository只實(shí)現(xiàn)一個(gè)數(shù)據(jù)源贩猎,則會(huì)顯得不太實(shí)用亿乳。需增加數(shù)據(jù)的持有坞嘀,當(dāng)用戶(hù)再次進(jìn)入界面時(shí)不用重新加載雨让,不然會(huì)浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬和迫使用戶(hù)等待新的請(qǐng)求尸变。為此,給UserRepository添加新的數(shù)據(jù)源艇棕,在內(nèi)存中緩存User對(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;
}
}
3.5.持久化數(shù)據(jù)
內(nèi)存緩存只能針對(duì)屏幕切換等當(dāng)前應(yīng)用進(jìn)程存在的情況串塑,但若進(jìn)程被系統(tǒng)殺死,則仍需重新網(wǎng)絡(luò)請(qǐng)求瓶颠。為了不使用移動(dòng)網(wǎng)絡(luò)重新獲取相同的數(shù)據(jù)拟赊,可以通過(guò)緩存Web請(qǐng)求來(lái)解決〈饬埽可當(dāng)場(chǎng)景是通過(guò)兩個(gè)不同類(lèi)型的請(qǐng)求來(lái)獲取相同類(lèi)型的數(shù)據(jù)時(shí)吸祟,會(huì)出現(xiàn)顯示不一致的問(wèn)題,導(dǎo)致需手動(dòng)合并它們桃移。正確的做法是數(shù)據(jù)持久化屋匕,在數(shù)據(jù)庫(kù)中完成合并操作。
Room是一個(gè)對(duì)象映射庫(kù)借杰,通過(guò)最少的樣板代碼實(shí)現(xiàn)本地?cái)?shù)據(jù)持久化过吻。在編譯時(shí)會(huì)驗(yàn)證每條查詢(xún)的樣式,將中斷SQL查詢(xún)的錯(cuò)誤反映在編譯期而不是運(yùn)行時(shí)蔗衡。Room封裝了一些與原始SQL表和查詢(xún)相關(guān)的底層實(shí)現(xiàn)細(xì)節(jié)纤虽,也允許觀察數(shù)據(jù)庫(kù)中數(shù)據(jù)(包括集合和連接查詢(xún))的變化,并通過(guò)LiveData對(duì)象來(lái)反映绞惦。此外逼纸,它還顯式地定義了線程約束來(lái)規(guī)避常見(jiàn)問(wèn)題,比如主線程上訪問(wèn)存儲(chǔ)济蝉。
如果熟悉其它持久化解決方案像SQLite ORM或不同的數(shù)據(jù)庫(kù)Realm杰刽,就不需要更換,除非Room的功能與用例很契合王滤。
// 創(chuàng)建表相關(guān)的樣式
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
// 創(chuàng)建數(shù)據(jù)訪問(wèn)對(duì)象
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
// 創(chuàng)建數(shù)據(jù)庫(kù)抽象類(lèi)贺嫂,編譯自動(dòng)實(shí)現(xiàn)
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
load
方法能直接返回LiveData<User>
,是很便利的雁乡。當(dāng)Room發(fā)現(xiàn)數(shù)據(jù)庫(kù)數(shù)據(jù)被改變了第喳,且至少有一個(gè)活躍的相關(guān)的觀察者,將自動(dòng)通知它們更新相關(guān)操作蔗怠。但是alpha1版本中墩弯,Room檢查變化是基于表的修改,有可能發(fā)些無(wú)效的修改通知寞射。
@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());
}
});
}
}
對(duì)UserRepository的改變不會(huì)影響UserProfileViewModel或者UserProfileFragment,這有利于測(cè)試锌钮,例如桥温,提供偽造的UserRepository來(lái)測(cè)試UserProfileViewModel。
有些情況下梁丘,例如下拉刷新侵浸,通過(guò)界面向用戶(hù)展示當(dāng)前進(jìn)行中的網(wǎng)絡(luò)操作是很重要的旺韭。好的做法是將界面操作與實(shí)際數(shù)據(jù)分離,因?yàn)閿?shù)據(jù)容易被各種原因更新(例如掏觉,獲取一個(gè)朋友列表区端,可能會(huì)再次獲取到相同的user并觸發(fā)LiveData更新)。從界面的角度來(lái)看澳腹,動(dòng)態(tài)請(qǐng)求實(shí)際上只是另一個(gè)數(shù)據(jù)點(diǎn)织盼,類(lèi)似其它任何數(shù)據(jù)片段(如,User對(duì)象)酱塔。這里有兩種常用解決方法:
- 給
getUser
方法返回的LiveData添加網(wǎng)絡(luò)操作的狀態(tài)沥邻。 - 在Repository中提供另一個(gè)公開(kāi)的方法以返回刷新的狀態(tài)。尤其適合專(zhuān)門(mén)響應(yīng)用戶(hù)操作(下拉刷新)羊娃,在界面中展示請(qǐng)求的網(wǎng)絡(luò)狀態(tài)唐全。
需注意單一數(shù)據(jù)源原則。不同的后端接口返回相同的數(shù)據(jù)(粒度不同)是一種常見(jiàn)情況蕊玷,但在請(qǐng)求的間隙邮利,服務(wù)端的數(shù)據(jù)可能發(fā)生改變。若Repository直接返回網(wǎng)絡(luò)請(qǐng)求垃帅,則導(dǎo)致界面顯示沖突延届。這就是為什么在上面UserRepository實(shí)現(xiàn)時(shí),網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)僅僅存到數(shù)據(jù)庫(kù)中挺智,觸發(fā)LiveData的刷新祷愉。推薦,DataBase是應(yīng)用唯一的數(shù)據(jù)源赦颇,而Repository是應(yīng)用其它部分的唯一數(shù)據(jù)源二鳄。
3.6.測(cè)試
很容易將代碼分成幾個(gè)模塊進(jìn)行測(cè)試。
- 用戶(hù)界面和交互:這是唯一使用設(shè)備界面測(cè)試的地方媒怯。最好使用Espresso測(cè)試界面代碼订讼,因?yàn)镕ragment只與ViewModel交互,提供一個(gè)mock的ViewModel足夠完整地測(cè)試這個(gè)界面扇苞。
- ViewModel:通過(guò)JUnit即可測(cè)試欺殿,僅mock所需的UserRepository。
- UserRepository:也可以通過(guò)JUnit測(cè)試鳖敷,僅mock所需的Webservice和UserDao脖苏。主要測(cè)試是否正常調(diào)用網(wǎng)絡(luò)服務(wù)和存儲(chǔ)結(jié)果到數(shù)據(jù)庫(kù),以及數(shù)據(jù)被緩存或更新后沒(méi)有多余的請(qǐng)求定踱。由于兩者都是接口棍潘,除了mock它們,還能為更復(fù)雜的測(cè)試用例模擬實(shí)現(xiàn)它們。
- UserDao:推薦僅使用設(shè)備測(cè)試亦歉。因?yàn)闇y(cè)試過(guò)程不涉及界面恤浪,仍能保持很快的運(yùn)行速度‰瓤可以創(chuàng)建內(nèi)存數(shù)據(jù)庫(kù)水由,以確保測(cè)試沒(méi)有任何副作用(如改變磁盤(pán)上的數(shù)據(jù)庫(kù)文件)。同時(shí)赛蔫,Room也允許指定數(shù)據(jù)庫(kù)砂客,通過(guò)提供SupportSQLiteOpenHelper的實(shí)現(xiàn)進(jìn)行單元測(cè)試。但通常不推薦濒募,因?yàn)槭謾C(jī)和電腦上的SQLite版本不一樣鞭盟。
- Webservice:測(cè)試應(yīng)該不依賴(lài)于外部,所以測(cè)試Webservice時(shí)不能通過(guò)網(wǎng)絡(luò)訪問(wèn)后臺(tái)瑰剃。有許多庫(kù)可以做到這點(diǎn)齿诉,例如,MockWebServer庫(kù)能為你的測(cè)試創(chuàng)建本地網(wǎng)絡(luò)服務(wù)晌姚。
- 測(cè)試工件:架構(gòu)組件提供一個(gè)
android.arch.core:core-testing
工件來(lái)控制后臺(tái)線程粤剧,包含兩個(gè)JUint規(guī)則:- InstantTaskExecutorRule:此規(guī)則可強(qiáng)制架構(gòu)組件在調(diào)用的線程上立即執(zhí)行任何后臺(tái)操作。
- CountingTaskExecutorRule:此規(guī)則能被用于設(shè)備測(cè)試中等待架構(gòu)組件的后臺(tái)操作或者連接到Espresso作為閑置資源挥唠。
3.7.最終架構(gòu)
4.指導(dǎo)原則
- 在Manifest中定義的入口點(diǎn)抵恋,如:acitivy,fragment宝磨,broadcast receiver等弧关,不是數(shù)據(jù)源。相反唤锉,它們應(yīng)該只是協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集世囊。由于每個(gè)應(yīng)用程序組件的存活時(shí)間很短,這取決于用戶(hù)與其設(shè)備的交互以及運(yùn)行時(shí)的總體狀況窿祥,所以任何入口點(diǎn)都不應(yīng)該成為數(shù)據(jù)源株憾。
- 嚴(yán)格的在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確的責(zé)任界限。例如:不要讓網(wǎng)絡(luò)加載數(shù)據(jù)的代碼被多個(gè)類(lèi)或包使用晒衩。同樣嗤瞎,不要將不相關(guān)的模塊(如:數(shù)據(jù)緩存和數(shù)據(jù)綁定)放到同一個(gè)類(lèi)中。
- 每個(gè)模塊盡可能少地暴露內(nèi)部听系。不要嘗試暴露模塊內(nèi)部哪怕一個(gè)地方的實(shí)現(xiàn)細(xì)節(jié)贝奇。你可能會(huì)在短期內(nèi)節(jié)省一些時(shí)間,但是隨著項(xiàng)目的發(fā)展靠胜,你將會(huì)花更多的時(shí)間來(lái)償還弃秆。
- 當(dāng)定義模塊間的交互時(shí)届惋,考慮如何讓每個(gè)模塊能獨(dú)立的測(cè)試髓帽。例如菠赚,擁有一個(gè)定義良好的、從網(wǎng)絡(luò)獲取數(shù)據(jù)的接口模塊郑藏,將會(huì)使持久化數(shù)據(jù)到本地?cái)?shù)據(jù)庫(kù)的行為更易于測(cè)試衡查。相反,如果將兩個(gè)模塊的邏輯放在一個(gè)地方必盖,或者將網(wǎng)絡(luò)相關(guān)的代碼擴(kuò)散到整個(gè)項(xiàng)目拌牲,測(cè)試將會(huì)變的非常困難(并非不可能)。
- 應(yīng)用程序的核心才是重點(diǎn)歌粥。不要花費(fèi)時(shí)間重復(fù)造輪子或一次又一次的編寫(xiě)相同的樣板代碼塌忽。相反,將精力集中在應(yīng)用程序的核心上失驶,讓AAC和其它優(yōu)秀的庫(kù)來(lái)處理重復(fù)的樣板代碼土居。
- 持久化盡可能多的相關(guān)最新數(shù)據(jù),以便應(yīng)用程序在設(shè)備處于離線模式時(shí)還可以使用嬉探。即使你可以享用穩(wěn)定高速的網(wǎng)絡(luò)連接擦耀,但是你的用戶(hù)可能無(wú)法享用。
- Repository應(yīng)該指定單一的數(shù)據(jù)源涩堤。每當(dāng)應(yīng)用程序需要訪問(wèn)數(shù)據(jù)時(shí)眷蜓,數(shù)據(jù)應(yīng)該始終來(lái)源于一個(gè)地方。
5.暴露網(wǎng)絡(luò)狀態(tài)
對(duì)于上面提到的網(wǎng)絡(luò)狀態(tài)問(wèn)題胎围,可以使用Resource類(lèi)封裝數(shù)據(jù)和狀態(tài)吁系。
//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);
}
}
從網(wǎng)絡(luò)獲取數(shù)據(jù)再將磁盤(pán)存儲(chǔ)的數(shù)據(jù)展示出來(lái),是一種常見(jiàn)的現(xiàn)象白魂,對(duì)于重復(fù)的邏輯(見(jiàn)下圖)可以提取出NetworkBoundResource類(lèi)來(lái)復(fù)用汽纤。
首先觀察數(shù)據(jù)庫(kù)的資源響應(yīng)數(shù)據(jù)的更新。當(dāng)從數(shù)據(jù)庫(kù)中獲取時(shí)碧聪,先判斷得到的結(jié)果是否符合使用要求冒版,不然就從網(wǎng)絡(luò)獲取。若想從網(wǎng)絡(luò)更新時(shí)顯示存儲(chǔ)數(shù)據(jù)逞姿,它們可以同時(shí)進(jìn)行辞嗡。若網(wǎng)絡(luò)加載成功,將結(jié)果存到數(shù)據(jù)庫(kù)中滞造,再次觸發(fā)加載流程续室;若加載失敗,直接調(diào)用失敗操作谒养。
新的數(shù)據(jù)存到磁盤(pán)上后挺狰,需從數(shù)據(jù)庫(kù)重新取數(shù)據(jù)明郭,若數(shù)據(jù)庫(kù)可以發(fā)送改變消息,通常不用這么做丰泊。但是薯定,依賴(lài)數(shù)據(jù)庫(kù)發(fā)送改變消息,也會(huì)有些問(wèn)題瞳购,因?yàn)閿?shù)據(jù)更新后可能并沒(méi)有變化话侄,數(shù)據(jù)庫(kù)將不會(huì)發(fā)送改變信息。同樣学赛,也不希望直接使用網(wǎng)絡(luò)請(qǐng)求到的結(jié)果年堆,因?yàn)檫@不符合單一數(shù)據(jù)源原則,哪怕也會(huì)觸發(fā)數(shù)據(jù)庫(kù)存儲(chǔ)更新盏浇。最后变丧,不希望沒(méi)新數(shù)據(jù)更新的情況下發(fā)送SUCCESS標(biāo)志,這會(huì)給客戶(hù)端錯(cuò)誤的信息绢掰。
// 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;
}
}
上面的類(lèi)定義兩種類(lèi)型參數(shù)(ResultType痒蓬,RequestType),是考慮到網(wǎng)絡(luò)獲取的數(shù)據(jù)類(lèi)型與本地使用的數(shù)據(jù)類(lèi)型不匹配曼月。而使用ApiResponse作為網(wǎng)絡(luò)請(qǐng)求谊却,是因?yàn)樗鼘?duì)
Retrofit2.Call
進(jìn)行簡(jiǎn)單的封裝,將返回類(lèi)型轉(zhuǎn)成LiveData哑芹。
對(duì)于上面提出的要求炎辨,可以通過(guò)以下具體實(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替換之前UserRepository的獲取數(shù)據(jù)的操作聪姿。由于Webservice和UserDao是由項(xiàng)目決定類(lèi)型碴萧,無(wú)法提取到NetworkBoundResource中,所以相關(guān)操作放在UserRepository中實(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();
}
}