相關文章:
- 【翻譯】安卓架構組件(2)-添加組件到你的項目中
- 【翻譯】安卓架構組件(3)-處理生命周期
- 【翻譯】安卓架構組件(4)-LiveData
- 【翻譯】安卓架構組件(5)-ViewModel
- 【翻譯】安卓架構組件(6)-Room持久化類庫
- 【翻譯】安卓架構組件(7)-分頁庫
說明:今年的Google I/O大會關于安卓的部分發布了全新的類庫:Architecture Components。這個新的類庫致力于從架構層面幫助你設計健壯、易于測試以及易于維護的app,其中包括UI組件生命周期的管理以及數據持久化等部分。我個人對這個類庫非常感興趣,很早就想寫一些關于這方面的文章,但是由于私人事務問題近期才有時間。我會先發布這個類庫相關文檔的譯文,在后面時間富裕的時候再聊一聊對這個類庫的理解和在實際應用的經歷。目前該類庫還處在alpha階段,但這并不影響我們對此的學習,當正式版放出后我相信會受到很多開發者的青睞
這份文檔用于已經掌握構建Android app基本技能,現在想要了解推薦的架構,想要實踐如何構建健壯、生產級別app的開發者。
本文檔假設讀者已經熟悉Android框架。如果你剛跟接觸Android,請訪問[這里]的訓練系列,該訓練包含了本文檔的所有預備知識。
app開發者所面臨的常見問題
與之對應的傳統桌面應用在大多數情況下含有一個單一的入口點(快捷圖標)并運行作為一個單一的程序,這和Android應用很不同。Android app擁有更復雜的結構。一個典型的Android app往往由多種組件構建而成,包括Activity
, Fragment
, Service
, Content Provider
以及Broadcast Receiver
。
這些app組件大部分被聲明在app清單文件(AndroidManifest)中,該清單文件被Android系統用于決定如何整合你的app到全局的用戶體驗中。如上文所說,傳統的桌面應用通常作為一個整體運行,而一個編寫良好的Android應用需要更加靈活,因為用戶常常在不同的app間頻繁切換。
例如,考慮當你想在你最喜歡的社交網絡上分享一張照片時會發生什么?app觸發一個相機的Intent
,Android系統啟動了一個相機應用來處理請求。在這個時候,用戶離開了該社交網絡app,但是在體驗上卻是無縫銜接的。接著,相機app可能觸發其他Intent來開啟其他應用,例如啟動文件選擇器。最終,用戶回到了社交網絡app并分享了圖片。同樣地,用戶可能在這一處理過程中的任何時刻被電話接聽所打斷,在接聽完成后繼續回來分享圖片。
在Android中,這種應用頻繁切換的行為很常見,因此你的app必須能夠正確處理這些行為。請記住,手機設備是被資源所約束的,因此在任何時候操作系統都有可能為了給新開啟的app騰出空間而殺死一些app。
關于這一切的關鍵點在于你的app組件可以單獨啟動并且是無序的,以及該組件可以在任何時候被用戶或系統銷毀。因為app組件是短暫的,并且它們的生命周期(例如何時創建以及何時銷毀)并不受你控制。你不能在你的app組件中存儲任何數據或狀態,并且你的組件之間不應該互相依賴。
常見架構原則
如果你不能使用app組件來存儲應用的數據和狀態,那么app該如何構建呢?
你所該關注最重要的事情是在你的app中遵守關注點分離原則。一個常見的錯誤是把你所有的代碼都寫在Activity
或者Fragment
中。任何不操作UI或操作系統交互的代碼都不應該放在上述這些類中。請盡量保持這些類的體積瘦小以避免許多生命周期相關的問題。不要忘記你并不擁有這些類,它們只是在你的應用和系統之間交互的粘合劑。安卓系統會在任何時候銷毀它們,例如用戶的交互行為或者其他因素,如可用內存過低等。為了提供一個可靠的用戶體驗,最好減少對它們的依賴。
第二個最重要的原則是你應該用模型驅動界面,最好是持久化模型(Persistent Model)。持久化是一個理想的狀態,理由如下:1.如果操作系統銷毀了你的應用來釋放資源,你的用戶不應該因此而丟掉數據。2.甚至當網絡堵塞甚至未連接時,你的應用應當繼續工作。Model是負責處理應用數據的組件,它們獨立于視圖(View)以及其他app組件,因此Model和這些生命周期相關的問題也是隔絕的。保持UI代碼的簡潔以及應用邏輯的自由更易于進行管理。將你的app基于Model類構建將對數據管理有利,并使得它們易于測試。
推薦app架構
在這一章節,我們致力于如何使用架構組件(Architecture Components)來構建一個app,我們將通過一個用例進行說明。
軟件工程領域沒有銀彈。我們不可能找到一種最佳的方法能夠一勞永逸地適合所有的場景。但是我們所推薦架構的意義在于對大多數用例來說都是好的。如果你已經有一個比較好的方式來寫Android應用,那么你不需要做出改變。
想象一下我們正在構建一個顯示用戶資料的UI界面。該用戶界面將通過REST API從我們的私有后臺獲取。
構建用戶界面
UI界面將會由一個叫做UserProfileFragment.java
的Fragment
和對應的布局文件user_profile_layout.xml
組成。
為了驅動UI界面,我們的數據模型需要持有兩個數據元素:
- User ID:用于區分用戶。通過
fragment
參數將信息傳遞至Fragment
是最佳的方式。如果Android系統銷毀了你的進程,這個信息將會被保存,因此當app下次重啟時,該id也將是可用的 - User Object:一個含有用戶數據的POJO類
我們將會創建一個基于ViewModel
類的UserProfileViewModel
來保存信息。
一個
ViewModel
提供了指定UI組件的數據,例如一個fragment
或activity
,并處理數據的交互,例如調用其他組件加載數據或數據的更新修改等。ViewModel
并不知道View
,也不受配置信息變化的影響,例如由于屏幕旋轉造成的Activity
重建。
現在我們擁有以下三個文件:
-
user_profile.xml
: 定義了屏幕的UI布局 -
UserProfileViewModel.java
:準備用于UI的數據類 -
UserProfileFragment.java
: UI控制器,在ViewModel
中顯示數據以及響應用戶交互
下面是我們的初步實現(布局文件比較簡單直接省略):
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);
}
}
如果你已經使用了類似于
RxJava
或者Agera
這樣的庫,你可以繼續使用它們,而不是LiveData
。但是如果當你使用它們,請確保正確地處理了生命周期,例如當相關的生命周期擁有者(LifecycleOwner)停止時應當暫停,當生命周期持有者銷毀時也應當銷毀。你也可以添加android.arch.lifecycle:reactivestreams
,使LiveData
和其他響應流式庫共同使用,例如RxJava
。
現在我們將UserProfileViewModel
中的User
成員變量替換為LiveData<User>
,使得當數據更新時,Fragment
可以收到通知。關于LiveData
一件很棒的事是,它能夠對生命周期做出反應,并將在不再需要的時候自動清除引用。
public class UserProfileViewModel extends ViewModel {
//...
private LiveData<User> user;//替換行
public LiveData<User> getUser() {
return user;
}
}
現在我們修改UserProfileFragment
,觀察數據變化并更新UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// 此處更新 UI
});
}
每次用戶數據被更新時,onChanged
回調函數會被調用,UI界面會被更新。
如果你熟悉其他使用觀察回調的類庫,你可能會意識到我們并沒有復寫Fragment
的onStop()
方法來停止對數據的觀察。這在LiveData
中是不必要的,因為它對生命周期敏感,這意味著將不會調用回調函數,除非Fragment
出在激活狀態(接收onStart()
但沒有接受onStop()
)。當Fragment
接收onDestroy()
方法時,LiveData
將會自動清除觀察者。
我們也不會做任何特殊的事情來處理配置的變化(例如旋轉屏幕)。當配置發生變化的時候,ViewModel
將會自動保存,因此一旦新的Fragment
到來時,它將會收到ViewModel
的相同實例,帶有當前數據的回調函數將會立即被調用。這就是ViewModel
不應該直接引用View
的原因,ViewModel
會在View
的生命周期外存活。詳見:[ViewModel的生命周期]。
獲取數據
現在我們將ViewModel和Fragment
關聯在了一起,但是ViewModel該如何獲取數據呢?在本例下,我們假設我們的后臺提供了REST API。我們會用Retrofit
庫來訪問我們的后臺,當然你可以隨意選擇其他不同的類庫。
這里就是和我們后臺交互的retrofit接口Webservice
:
public interface Webservice {
/**
* @GET 聲明是一個HTTP GET請求
* @Path("user") 標記了userId參數來替換GET請求中的{user}路徑
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
關于Retrofit的使用請詳見官方文檔,這里只是簡單進行了說明
ViewModel
的原生實現可以直接調用Webservice
來獲取數據并交給用戶對象。即使這樣可以生效,你的app將會隨著增長而難以維護。相對于我們上文所提到的關注點分離原則,這種方式給予了ViewModel
類太多的職責。另外ViewModel
的作用于被綁在Activity
或Fragment
的生命周期上,因此當生命周期結束的時候丟掉這些數據是一種很糟糕的用戶體驗。作為替代,我們的ViewModel
將會把這一工作委派給新的倉庫(Repository)模塊。
倉庫模塊(Repository Module)負責處理數據操作。他們提供了清晰的API,并且知道在哪獲取數據以及哪種API的調用會導致數據更新。你可以考慮把它作為多種數據源的中介(持久化模型,網絡服務數據,緩存等)。
下方的UserRepository
類將會使用WebService
來獲取數據項:
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// 這并不是最佳的實現方式,我們將在下文修正它
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// 錯誤情況的處理被省略了
data.setValue(response.body());
}
});
return data;
}
}
即使倉庫模型看起來并不需要,但是它完成了一個重要的目標:它將app中的數據源抽象了出來。現在我們的ViewModel
不知道數據是由Webservice
獲取而來的,這意味著在需要其他實現的時候我們可以進行替換。
管理組件間的依賴
上面的UserRepository
類需要WebService
接口的一個實例去進行工作。我們當然可以在每個倉庫模型類中簡單地創建一個,不過需要知道WebService
所依賴的具體子類。這將會顯著提高代碼的復雜性和冗余。另外UserRepository
也可能不是唯一需要WebService
的類,如果每個類都創建一個WebService
,這將會浪費很多的資源。
有兩種模式可以解決這個問題:
- 依賴注入:依賴注入允許類定義依賴而不用去構造他們。在運行的時候,另一個類負責提供這些依賴關系。我們推薦在安卓中使用谷歌的[Dagger 2]類庫進行依賴注入。通過遍歷依賴樹,Dagger 2 自動構造對象并提供編譯時的依賴保障。
- 服務定位:服務定位器提供了注冊器,使得類可通過依賴進行構建,而不是需要配置它們。服務定位模式相對依賴注入而言更易于實現,因此如果你并不熟悉依賴注入,可以使用服務定位來代替。
連接ViewModel
和倉庫
現在我們修改我們的UserProfileViewModel
以使用倉庫:
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository 參數由Dagger 2提供
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel 由每個fragment創建,因此我們知道并不會發生改變
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}
緩存數據
上述倉庫的實現易于抽象了調用網絡服務的過程,但是因為它僅僅依賴于一個單一的數據源,因此并不是很實用。
UserRepository
實現的問題在于在獲取數據以后,并沒有在任何地方保存它。如果用戶離開了UserProfileFragment
并再次回來,app會重新獲取數據。這很糟糕,有以下兩個原因:1.浪費了寶貴的網絡帶寬;2.強迫用戶等待新的請求完成。為了解決這個問題,我們將在UserRepository
添加一個新的數據源在內存中緩存我們的User
對象。
@Singleton // 通知 Dagger 該類應該只構建一次
public class UserRepository {
private Webservice webservice;
// 簡單緩存在內存中,忽略實現細節
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);
// 這仍然不是最優的代碼,但是要比之前的代碼好
// 一個完整的實現必須處理錯誤情況
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}
數據持久化
在我們當前的實現中,如果用戶旋轉了屏幕或者離開并返回app,當前UI界面將立刻可見,這是因為倉庫從內存中獲取了數據。但是如果用戶離開app很久,在Android系統殺掉進程后再回來呢?
在當前的實現中,我們需要從網絡重新獲取數據。這并不僅是一個很糟糕的用戶習慣,并且很浪費,因為我們要重新獲取相同的數據。你可以僅僅通過緩存網絡請求來修復它,但是這也創造了新的問題。如果相同的數據類型在另一個請求中發生(如獲取一組好友列表)呢?如果是這樣,你的app可能會顯示不正確的數據。
正確解決這個問題的關鍵在于使用一個持久化模型。這正是Room
持久化類庫所解決的問題。
Room
是一個以最小化模板代碼提供本地數據持久化的對象關系映射類庫。在編譯時間,它會驗證每個查詢語句,因此錯誤的SQL會導致編譯時報錯,而不是在運行時報錯。Room
抽象了一些原生SQL表和查詢的底層實現細節。它也允許觀察數據庫數據的變化,通過LiveData
對象進行展現。此外,它顯式地定義線程約束以解決一些常見的問題,如在主線程訪問存儲。
如果你對另一些持久化解決方案很熟悉,你并不需要進行替換,除非
Room
的功能集和你的用例更符合。
為了使用Room
,我們需要定義我們的本地表。首先使用@Entity
去注解User
類,標記該類作為數據庫中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters/setters
}
之后,通過擴展RoomDatabase
類創建一個數據庫類:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意,MyDatabase
類是抽象的,Room
會自動提供實現。詳情請參見Room
文檔。
現在我們需要一個方式將用戶數據插入到數據庫中,為此我們需要創建一個數據訪問對象(DAO):
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
之后,從我們的數據庫類中引用DAO:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請注意load
方法返回了一個LiveData<User>
。Room
知道數據庫什么時候被修改并將在數據變化時自動通知所有已激活的觀察者。使用了LiveData
是很高效的,因為只有在至少含有一個處在激活狀態的觀察者時才會更新。
目前處在alpha 1版本中,
Room
會檢查基于表修改的錯誤信息,也就是說會分發假陽性的通知。假陽性是指分發的通知是正確的,但是并非是由數據變化所造成的。
現在我們修改UserRepository
類,將Room
數據源包含在內。
@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 userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// 運行在后臺線程
// 檢查用戶最新是否獲取更新
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// 刷新數據
Response response = webservice.getUser(userId).execute();
// TODO 錯誤情況監測處理(省略)
// 更新數據庫,LiveData會自動更新,因此只需要更新數據庫就可以了
userDao.save(response.body());
}
});
}
}
請注意,即使我們在UserRepository
中改變了數據源,我們仍然不需要改變UserProfileViewModel
或者UserProfileFragment
。這種靈活性是由抽象所提供的。這對于測試來說也是很棒的,因為你可以在測試UserProfileViewModel
的時候提供一個假的UserRepository
。
現在我們的代碼完成了。如果用戶稍后再次回到相同的UI,將會立即看到用戶信息,因為我們進行了持久化。同時,如果數據過時了,我們的倉庫會在后臺更新數據它們。當然這取決于你的具體用例,你可以選擇在數據過時的時候不顯示它們。
在一些用例中,例如pull-to-refresh,對于UI來說如果當前在進行網絡請求,對用戶顯示該進度是很重要的。將UI的行為和實際數據分離是一種很好的實踐,因為數據可能因為多種原因被更新(例如如果我們拉取一組朋友列表,已存在的數據可能會被再次獲取,從而觸發了LiveData<User>
更新)。從UI的角度來看,事實上是另一個數據端。
該用例有兩個常見的方案:
- 修改
getUser()
方法,返回帶有網絡操作狀態的LiveData
,例如下文中的“顯示網絡狀態”章節。 - 在倉庫類中提供另一個公共方法,返回
User
類的刷新狀態。這種方式更好,如果你想要僅僅在響應顯式地用戶操作(如pull-to-refresh)時顯示網絡狀態。
真正單一數據源
對于不同的REST API返回相同的數據是很常見的,例如,如果我們的后臺有另一個接口用于返回朋友列表,相同的User
對象會從兩個API返回。如果UserRepository
也要去返回Webservice
請求的結果,我們的UI界面可能會顯示不正常數據,因為數據可能會因這兩個請求接口而改變。這也就是為什么在UserRepository
實現中,網絡服務僅僅存儲數據到數據庫的原因。之后,數據庫信息的改變會觸發LiveData
的更新。
在這種模型下,數據庫作為單一數據源,而app的其他部分通過倉庫進行訪問。不論你是否使用持久化存儲,我們推薦你的倉庫指定一個數據源作為app的單一數據源。
測試
關注點分離原則一個很重要的受益處在于可測試性。讓我們看看每個模塊代碼的測試。
- UI&交互:這是唯一需要[Android UI Instrumentation test]的時刻。測試UI的最佳方式是創建一個[Espresso]特使。你可以創建
Fragment
并提供一個虛擬的ViewModel
。因為Fragment
僅僅和ViewModel
對話,模擬ViewModel
對于測試來說就已經足夠了。 - ViewModel:
ViewModel
可以使用[JUnit測試]。你僅僅需要模擬UserRepository
。 - UserRepository:你也可以使用
JUnit
測試UserRepository
。你需要模擬Webservice
和DAO。你可以測試網絡請求調用,在數據庫中保存結果,以及如果數據被緩存并更新后不需要進行請求。因為Webservice
和UserDao
都是接口,你可以模擬它們。 - UserDao:測試DAO類的推薦方法是使用測試工具。因為這些測試工具并不需要任何的UI并運行速度很快。對每個測試來說,你可以創建一個內存數據庫來保證測試并不會造成雙邊效應(如改變磁盤上數據庫的已有數據)。
- WebService:獨立于外部世界的測試是很重要的,甚至你的
Webservice
測試應該避免調用后臺的網絡服務。有大量的類庫可以幫助做到這一點,例如:[MockWebServer]。 - 測試構件:架構組件提供一個Maven構件來控制后臺線程。在
android.arch.core:core-testing
中,有兩個JUnit
規則:- 任務立即執行規則:這個規則可用于強制架構組件在調用線程里立即執行任何后臺操作
- 這個規則可用于工具測試,以等待架構組件的后臺操作或者連接至
Espresso
作為閑置資源。
最終架構
下圖顯示了我們所推薦架構的所有模塊,以及相互間的交互情況:
指導原則
以下的建議并不是強制性的,而是根據我們的經驗得知,遵循這些建議會使你的代碼更健壯,易于測試和易于維護。
- 你在清單文件中所定義的入口點——Activity,Service,Broadcast Receiver等并不應該是數據源。相反,他們應該僅僅是和入庫點相關的數據源子集。因為每個app的組件的存活時間都是短暫的、取決于用戶的交互行為以及運行時整體上的健康度。
- 殘忍堅決地創建良好的模塊分界。例如,不要將從網絡讀取數據的代碼擴展到多個類/包中。相似地,也不要將不相關職責的代碼添加進來,如數據緩存等。(高內聚,低耦合)
- 模塊間交互暴露的接口應該盡可能的少。不要嘗試創建“僅僅用一次”的捷徑,導致暴露一個模塊的內部實現細節。你可能在短期會獲益,但是在代碼的演進過程中會耗費數倍的技術負擔。
- 當你定義了模塊間的交互時,考慮每個模塊的單獨可測試性。例如,有一個定義良好的用于從網絡獲取數據的API會更易于測試本地數據庫持久化的模塊。相反,如果你搞亂了兩個模塊間的邏輯,或將你網絡請求的代碼鋪滿了所有的地方,那么這將很難進行測試。
- 你app的核心是如何在其他app中變得突出。不要花費時間重復造輪子或一遍一遍地寫相同的模板代碼。相反,將你的心思花在如何使你的app獨一無二,讓Android架構組件以及其他推薦類庫處理重復的部分。
- 持久化盡可能多和盡可能新鮮的數據,這樣在離線模式下你的app也是可用的。你可能很享受高速的網絡連接,可你的用戶并不一定這樣認為。
- 你的倉庫應當指定單一數據源。當你的app需要訪問數據時,應該永遠來自于這個單一的數據源。
附加:顯示網絡狀態
在“推薦app架構”一節中,我們故意忽略了網絡錯誤和加載狀態,以使樣例代碼更簡單。在本節中,我們致力于使用Resource
類顯示網絡狀態以及數據本身。
下面是樣例的實現:
//一個描述數據以及其狀態的泛型類
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);
}
}
因為從網絡加載數據并進行顯示是一個常見的用例,我們創建了一個幫助類NetworkBoundResource
可以在多個地方復用。下圖是NetworkBoundResource
的決策樹:
起點從觀察數據源(數據庫)開始。當入口被數據庫第一次加載時,NetworkBoundResource
檢查結果是否足夠良好以至于可以分發,并且/或應該從網絡進行獲取。注意,這二者可以同時發生,因為你可能想要顯示緩存,同時從網絡更新數據。
如果網絡調用完全成功,保存結果至數據庫并重新初始化數據流。如果網絡請求失敗,我們直接分發一個錯誤。
將新的數據存儲到磁盤以后,我們從數據庫重新初始化數據流,但是通常我們并不需要這樣做,因為數據庫會分發這次變化。另一方面,依賴數據庫去分發變化會是一把雙刃劍,如果數據并沒有變化,我們實際上可以避免這次分發。我們也不分發網絡請求得到的數據,因為這違反了單一數據源的原則。
以下是NetworkBoundResource
所提供的API:
// ResultType: 數據源類型
// RequestType: API返回的類型
public abstract class NetworkBoundResource<ResultType, RequestType> {
// 被調用保存API返回的結果至數據庫
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// 被調用去判斷是否應該從網絡獲取數據
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// 被調用從數據庫獲取緩存數據
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// 被調用創建API請求
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// 當獲取數據失敗時候調用
@MainThread
protected void onFetchFailed() {
}
// 返回代表數據源的LiveData
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
注意,上面的類定義了兩種類型的參數(ResultType
和RequestType
),因為從API返回的數據類型可能和本地的數據類型并不匹配。
同樣也請注意,上面的代碼使用了ApiResponse
用于網絡請求。ApiResponse
是Retrofit2.Call
類的簡單包裝,用于將返回結果轉化為LiveData
。
下面的NetworkBoundResource
的其余實現:
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();
// 重新連接dbSource作為新的源,
//這樣會快速分發最新的數據
result.addSource(dbSource,
newData -> result.setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
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) {
// 我們專門請求一個新的LiveData
// 另一方面獲取最新的緩存數據,可能并不是網絡請求得到的最新數據
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}
現在,我們使用NetworkBoundResource
來重寫UserRepository
:
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();
}
}