官方文檔鏈接:https://developer.android.google.cn/topic/libraries/architecture/guide.html
1.前言
由于用戶在使用某一功能時(shí)會(huì)涉及不同的應(yīng)用程序,需要不斷地切換流程和任務(wù)。舉個(gè)例子,在使用社交軟件分享照片時(shí),會(huì)需要打開攝像頭,也會(huì)需要從圖庫中選擇文件,而將數(shù)據(jù)返回社交軟件的過程中,有可能會(huì)有電話打來。這一下子啟動(dòng)了許多應(yīng)用程序,但是移動(dòng)設(shè)備資源有限,操作系統(tǒng)隨時(shí)會(huì)殺死一些應(yīng)用程序來騰出空間給新的應(yīng)用。所以應(yīng)用程序組件的存在不由開發(fā)者控制,不應(yīng)該存儲(chǔ)數(shù)據(jù)或狀態(tài),更不應(yīng)該彼此依賴。
之前通過生命周期進(jìn)行相關(guān)操作,但這引入另一套邏輯,增加代碼的復(fù)雜度。
2.常見架構(gòu)原則
2.1.View層分離
使Activity和Fragment中的代碼盡可能的少,只處理與界面或與操作系統(tǒng)的交互。因?yàn)檫@些類是操作系統(tǒng)和應(yīng)用程序的中間件,不由開發(fā)者控制,所以應(yīng)該最小化依賴。
2.2.Model層驅(qū)動(dòng)
通過持久化模型驅(qū)動(dòng)界面,具有兩點(diǎn)好處:
- 當(dāng)操作系統(tǒng)釋放組件資源時(shí),用戶數(shù)據(jù)也不會(huì)丟失;
- 當(dāng)網(wǎng)絡(luò)連接狀態(tài)不好時(shí),應(yīng)用程序也能正常工作。
持久化模型獨(dú)立于組件,不受系統(tǒng)的控制。基于它的應(yīng)用程序可以保證界面代碼簡單,同時(shí)業(yè)務(wù)邏輯具有可測試性。
3.推薦的應(yīng)用框架
沒有一種架構(gòu)適用于所有場景。意味著,這是一個(gè)好的起點(diǎn),但若已經(jīng)有更適合的了,是不需要改變的。以網(wǎng)絡(luò)獲取并顯示用戶配置信息為例子,展示架構(gòu)組件的使用方式。
3.1.建立用戶界面
界面包含一個(gè)UserProfileFragment.java的組件類和user_profile_layout.xml的布局文件。為了驅(qū)動(dòng)界面,基于ViewModel類創(chuàng)建UserProfileViewModel.java的數(shù)據(jù)模型來保存信息。數(shù)據(jù)元素主要包括:
- The User ID:用戶標(biāo)識(shí)符。最好通過
Fragment Arguments傳入Fragment,那樣操作系統(tǒng)銷毀應(yīng)用進(jìn)程時(shí),id將被保存以便重啟時(shí)使用。 - The User object:一個(gè)POJO類保存用戶數(shù)據(jù)。
一個(gè)ViewModel給一個(gè)特定的界面組件(Activity或Fragment)提供數(shù)據(jù),并處理業(yè)務(wù)邏輯中數(shù)據(jù)相關(guān)的操作,如調(diào)用其它組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶的修改。ViewModel與View隔離,不受屏幕旋轉(zhuǎn)導(dǎo)致重建等Configuration Changes的影響。而Activity或Fragment則是UI Controller,與用戶行為和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)定后,支持庫中的Fragment將實(shí)現(xiàn)LifecycleOwner接口。
ViewModel與Presenter最大的不同是,Presenter內(nèi)部封裝著一系列的行為,而ViewModel持有的是數(shù)據(jù)(狀態(tài)),那么數(shù)據(jù)如何傳遞呢?這里就得提LiveData了。
LiveData是一個(gè)可被觀察的數(shù)據(jù)持有者。它讓應(yīng)用中的組件觀察自己的變化,卻不需要顯式的和剛性的依賴。LiveData同時(shí)會(huì)監(jiān)聽?wèi)?yīng)用組件(Activity,F(xiàn)ragment,Services)的生命周期狀態(tài),并且做正確的事情來防止對象的內(nèi)存泄露。
如果已經(jīng)使用RxJava或Agera庫,你可以繼續(xù)使用來替代LiveData。不過當(dāng)你使用它們時(shí),確保正確處理生命周期,例如:LifecycleOwner調(diào)用了
onStop()方法,需暫停相關(guān)的數(shù)據(jù)流;LifecycleOwner調(diào)用了onDestory()方法,需銷毀相關(guān)的數(shù)據(jù)流。你也可以添加android.arch.lifecycle:reactivestreams工件來讓LiveData和其它的響應(yīng)流庫(RxJava2)一起使用。
現(xiàn)在,用LiveData<User>來替代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)的庫不同。當(dāng)Fragment不處于活躍狀態(tài)時(shí),是不會(huì)回調(diào)的,所以不需要手動(dòng)在Fragment的onStop()方法中停止觀察數(shù)據(jù);當(dāng)Fragment銷毀了,LiveData將會(huì)移除觀察者,釋放資源。
不需要做額外的事,ViewModel在配置改變時(shí),將自動(dòng)恢復(fù)相同的ViewModel實(shí)例及對當(dāng)前數(shù)據(jù)的回調(diào)。
3.2.獲取數(shù)據(jù)
將ViewModel和Fragment連接后,還需要ViewModel獲取到用戶數(shù)據(jù)。這里,假設(shè)使用Retrofit庫來訪問后端接口。
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對象,但隨著項(xiàng)目的變大,將越來越難以維護(hù),而且給ViewModel太多的職責(zé),與單一職責(zé)的原則相違背。此外,ViewModel的活動(dòng)范圍與Activity或Fragment的生命周期相關(guān),即生命周期結(jié)束后將丟失所有數(shù)據(jù)。為此,引入了Repository這個(gè)概念。
Repository是專門用來處理數(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ú)立出來,ViewModel不知道與什么數(shù)據(jù)源交互,便于替換。但是UserRepository需要Webservice實(shí)例,若通過構(gòu)造方法提供,每個(gè)使用Webservice的類都得知道它的構(gòu)造方法,使依賴變得復(fù)雜,同時(shí)都創(chuàng)建對象將占用大量資源。這里提供兩種方法參考:
- 依賴注入:允許類定義它們之間的依賴而不需要立馬構(gòu)建出來,等到運(yùn)行時(shí),由其它的類提供這些依賴。
- 服務(wù)定位:提供一個(gè)倉庫,類可以從中獲取它們所需的依賴而不是創(chuàng)建它們。相較于依賴注入,它更容易實(shí)現(xiàn)。
這些模式可以方便地?cái)U(kuò)展代碼,因?yàn)樘峁┝饲逦姆绞焦芾硪蕾?,而不需要增加其它代碼。更重要的是它們都很容易測試。
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)用戶再次進(jìn)入界面時(shí)不用重新加載,不然會(huì)浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬和迫使用戶等待新的請求。為此,給UserRepository添加新的數(shù)據(jù)源,在內(nèi)存中緩存User對象。
@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)存緩存只能針對屏幕切換等當(dāng)前應(yīng)用進(jìn)程存在的情況,但若進(jìn)程被系統(tǒng)殺死,則仍需重新網(wǎng)絡(luò)請求。為了不使用移動(dòng)網(wǎng)絡(luò)重新獲取相同的數(shù)據(jù),可以通過緩存Web請求來解決。可當(dāng)場景是通過兩個(gè)不同類型的請求來獲取相同類型的數(shù)據(jù)時(shí),會(huì)出現(xiàn)顯示不一致的問題,導(dǎo)致需手動(dòng)合并它們。正確的做法是數(shù)據(jù)持久化,在數(shù)據(jù)庫中完成合并操作。
Room是一個(gè)對象映射庫,通過最少的樣板代碼實(shí)現(xiàn)本地?cái)?shù)據(jù)持久化。在編譯時(shí)會(huì)驗(yàn)證每條查詢的樣式,將中斷SQL查詢的錯(cuò)誤反映在編譯期而不是運(yùn)行時(shí)。Room封裝了一些與原始SQL表和查詢相關(guān)的底層實(shí)現(xiàn)細(xì)節(jié),也允許觀察數(shù)據(jù)庫中數(shù)據(jù)(包括集合和連接查詢)的變化,并通過LiveData對象來反映。此外,它還顯式地定義了線程約束來規(guī)避常見問題,比如主線程上訪問存儲(chǔ)。
如果熟悉其它持久化解決方案像SQLite ORM或不同的數(shù)據(jù)庫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ù)訪問對象
@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ù)庫抽象類,編譯自動(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ù)庫數(shù)據(jù)被改變了,且至少有一個(gè)活躍的相關(guān)的觀察者,將自動(dòng)通知它們更新相關(guān)操作。但是alpha1版本中,Room檢查變化是基于表的修改,有可能發(fā)些無效的修改通知。
@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的改變不會(huì)影響UserProfileViewModel或者UserProfileFragment,這有利于測試,例如,提供偽造的UserRepository來測試UserProfileViewModel。
有些情況下,例如下拉刷新,通過界面向用戶展示當(dāng)前進(jìn)行中的網(wǎng)絡(luò)操作是很重要的。好的做法是將界面操作與實(shí)際數(shù)據(jù)分離,因?yàn)閿?shù)據(jù)容易被各種原因更新(例如,獲取一個(gè)朋友列表,可能會(huì)再次獲取到相同的user并觸發(fā)LiveData更新)。從界面的角度來看,動(dòng)態(tài)請求實(shí)際上只是另一個(gè)數(shù)據(jù)點(diǎn),類似其它任何數(shù)據(jù)片段(如,User對象)。這里有兩種常用解決方法:
- 給
getUser方法返回的LiveData添加網(wǎng)絡(luò)操作的狀態(tài)。 - 在Repository中提供另一個(gè)公開的方法以返回刷新的狀態(tài)。尤其適合專門響應(yīng)用戶操作(下拉刷新),在界面中展示請求的網(wǎng)絡(luò)狀態(tài)。
需注意單一數(shù)據(jù)源原則。不同的后端接口返回相同的數(shù)據(jù)(粒度不同)是一種常見情況,但在請求的間隙,服務(wù)端的數(shù)據(jù)可能發(fā)生改變。若Repository直接返回網(wǎng)絡(luò)請求,則導(dǎo)致界面顯示沖突。這就是為什么在上面UserRepository實(shí)現(xiàn)時(shí),網(wǎng)絡(luò)請求的數(shù)據(jù)僅僅存到數(shù)據(jù)庫中,觸發(fā)LiveData的刷新。推薦,DataBase是應(yīng)用唯一的數(shù)據(jù)源,而Repository是應(yīng)用其它部分的唯一數(shù)據(jù)源。
3.6.測試
很容易將代碼分成幾個(gè)模塊進(jìn)行測試。
- 用戶界面和交互:這是唯一使用設(shè)備界面測試的地方。最好使用Espresso測試界面代碼,因?yàn)镕ragment只與ViewModel交互,提供一個(gè)mock的ViewModel足夠完整地測試這個(gè)界面。
- ViewModel:通過JUnit即可測試,僅mock所需的UserRepository。
- UserRepository:也可以通過JUnit測試,僅mock所需的Webservice和UserDao。主要測試是否正常調(diào)用網(wǎng)絡(luò)服務(wù)和存儲(chǔ)結(jié)果到數(shù)據(jù)庫,以及數(shù)據(jù)被緩存或更新后沒有多余的請求。由于兩者都是接口,除了mock它們,還能為更復(fù)雜的測試用例模擬實(shí)現(xiàn)它們。
- UserDao:推薦僅使用設(shè)備測試。因?yàn)闇y試過程不涉及界面,仍能保持很快的運(yùn)行速度??梢詣?chuàng)建內(nèi)存數(shù)據(jù)庫,以確保測試沒有任何副作用(如改變磁盤上的數(shù)據(jù)庫文件)。同時(shí),Room也允許指定數(shù)據(jù)庫,通過提供SupportSQLiteOpenHelper的實(shí)現(xiàn)進(jìn)行單元測試。但通常不推薦,因?yàn)槭謾C(jī)和電腦上的SQLite版本不一樣。
- Webservice:測試應(yīng)該不依賴于外部,所以測試Webservice時(shí)不能通過網(wǎng)絡(luò)訪問后臺(tái)。有許多庫可以做到這點(diǎn),例如,MockWebServer庫能為你的測試創(chuàng)建本地網(wǎng)絡(luò)服務(wù)。
- 測試工件:架構(gòu)組件提供一個(gè)
android.arch.core:core-testing工件來控制后臺(tái)線程,包含兩個(gè)JUint規(guī)則:- InstantTaskExecutorRule:此規(guī)則可強(qiáng)制架構(gòu)組件在調(diào)用的線程上立即執(zhí)行任何后臺(tái)操作。
- CountingTaskExecutorRule:此規(guī)則能被用于設(shè)備測試中等待架構(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í)間很短,這取決于用戶與其設(shè)備的交互以及運(yùn)行時(shí)的總體狀況,所以任何入口點(diǎn)都不應(yīng)該成為數(shù)據(jù)源。
- 嚴(yán)格的在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確的責(zé)任界限。例如:不要讓網(wǎng)絡(luò)加載數(shù)據(jù)的代碼被多個(gè)類或包使用。同樣,不要將不相關(guān)的模塊(如:數(shù)據(jù)緩存和數(shù)據(jù)綁定)放到同一個(gè)類中。
- 每個(gè)模塊盡可能少地暴露內(nèi)部。不要嘗試暴露模塊內(nèi)部哪怕一個(gè)地方的實(shí)現(xiàn)細(xì)節(jié)。你可能會(huì)在短期內(nèi)節(jié)省一些時(shí)間,但是隨著項(xiàng)目的發(fā)展,你將會(huì)花更多的時(shí)間來償還。
- 當(dāng)定義模塊間的交互時(shí),考慮如何讓每個(gè)模塊能獨(dú)立的測試。例如,擁有一個(gè)定義良好的、從網(wǎng)絡(luò)獲取數(shù)據(jù)的接口模塊,將會(huì)使持久化數(shù)據(jù)到本地?cái)?shù)據(jù)庫的行為更易于測試。相反,如果將兩個(gè)模塊的邏輯放在一個(gè)地方,或者將網(wǎng)絡(luò)相關(guān)的代碼擴(kuò)散到整個(gè)項(xiàng)目,測試將會(huì)變的非常困難(并非不可能)。
- 應(yīng)用程序的核心才是重點(diǎn)。不要花費(fèi)時(shí)間重復(fù)造輪子或一次又一次的編寫相同的樣板代碼。相反,將精力集中在應(yīng)用程序的核心上,讓AAC和其它優(yōu)秀的庫來處理重復(fù)的樣板代碼。
- 持久化盡可能多的相關(guān)最新數(shù)據(jù),以便應(yīng)用程序在設(shè)備處于離線模式時(shí)還可以使用。即使你可以享用穩(wěn)定高速的網(wǎng)絡(luò)連接,但是你的用戶可能無法享用。
- Repository應(yīng)該指定單一的數(shù)據(jù)源。每當(dāng)應(yīng)用程序需要訪問數(shù)據(jù)時(shí),數(shù)據(jù)應(yīng)該始終來源于一個(gè)地方。
5.暴露網(wǎng)絡(luò)狀態(tài)
對于上面提到的網(wǎng)絡(luò)狀態(tài)問題,可以使用Resource類封裝數(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ù)再將磁盤存儲(chǔ)的數(shù)據(jù)展示出來,是一種常見的現(xiàn)象,對于重復(fù)的邏輯(見下圖)可以提取出NetworkBoundResource類來復(fù)用。

首先觀察數(shù)據(jù)庫的資源響應(yīng)數(shù)據(jù)的更新。當(dāng)從數(shù)據(jù)庫中獲取時(shí),先判斷得到的結(jié)果是否符合使用要求,不然就從網(wǎng)絡(luò)獲取。若想從網(wǎng)絡(luò)更新時(shí)顯示存儲(chǔ)數(shù)據(jù),它們可以同時(shí)進(jìn)行。若網(wǎng)絡(luò)加載成功,將結(jié)果存到數(shù)據(jù)庫中,再次觸發(fā)加載流程;若加載失敗,直接調(diào)用失敗操作。
新的數(shù)據(jù)存到磁盤上后,需從數(shù)據(jù)庫重新取數(shù)據(jù),若數(shù)據(jù)庫可以發(fā)送改變消息,通常不用這么做。但是,依賴數(shù)據(jù)庫發(fā)送改變消息,也會(huì)有些問題,因?yàn)閿?shù)據(jù)更新后可能并沒有變化,數(shù)據(jù)庫將不會(huì)發(fā)送改變信息。同樣,也不希望直接使用網(wǎng)絡(luò)請求到的結(jié)果,因?yàn)檫@不符合單一數(shù)據(jù)源原則,哪怕也會(huì)觸發(fā)數(shù)據(jù)庫存儲(chǔ)更新。最后,不希望沒新數(shù)據(jù)更新的情況下發(fā)送SUCCESS標(biāo)志,這會(huì)給客戶端錯(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;
}
}
上面的類定義兩種類型參數(shù)(ResultType,RequestType),是考慮到網(wǎng)絡(luò)獲取的數(shù)據(jù)類型與本地使用的數(shù)據(jù)類型不匹配。而使用ApiResponse作為網(wǎng)絡(luò)請求,是因?yàn)樗鼘?code>Retrofit2.Call進(jìn)行簡單的封裝,將返回類型轉(zhuǎn)成LiveData。
對于上面提出的要求,可以通過以下具體實(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)目決定類型,無法提取到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();
}
}