此指南適用於那些曾經或現在進行Android應用的基礎開發,並希望了解和學習編寫Android程序的最佳實踐和架構。通過學習來構建強大的生產級別的應用。
注意:此指南默認你對Android開發有比較深的理解,熟知Android Framework。如果你還只是個Android開發新手,那么建議先學習下Android的基礎知識。
Android程序員面臨的問題
傳統的桌面應用程序開發在大多數情況下,啟動器快捷方式都有一個入口點,並作為一個單一的過程運行,但Android應用程序的結構更為復雜。典型的Android應用程序由多個應用程序組件構成,包括Activity,Fragment,Service,ContentProvider和Broadcast Receiver。
大多數這些應用程序組件在Android操作系統使用的AndroidManifest中聲明,以決定如何將應用程序集成到設備上來為用戶提供完整的體驗。盡管如前所述,桌面應用程序傳統上是作為一個單一的進程運行的,但正確編寫的Android應用程序則需要更靈活,因為用戶通過設備上的不同應用程序編織方式,不斷切換流程和任務。
舉個例子,當用戶在社交App上打算分享一張照片,那么Android系統就會為此啟動相機來完成此次請求。此時用戶離開了社交App,但是這個用戶體驗是無縫連接的。相機可能又會觸發並啟動文件管理器來選擇照片。最終回到社交App並分享照片。此外,在此過程中的任何時候,用戶可能會被打電話中斷,並在完成電話后再回來分享照片。
在Android中,這種應用間跳轉行為很常見,因此你的應用必須正確處理這些流程。請記住,移動設備是資源有限的,所以在任何時候,操作系統可能需要殺死一些應用來為新的應用騰出空間。
你的應用程序的所有組件都可以被單獨啟動或無序啟動,並且在任何時候由用戶或系統銷毀。因為應用程序組件是短暫的,它們的生命周期(創建和銷毀時)不受你的控制,因此你不應該將任何應用程序數據或狀態存儲在應用程序組件中,並且應用程序組件不應相互依賴。
常見的架構原理
如果你無法使用應用程序組件來存儲應用程序數據和狀態,應如何構建應用程序?
在你的App開發中你應該將重心放在分層上,如果將所有的代碼都寫在Activity或者Fragment中,那問題就大了。任何不是處理UI或跟操作系統交互的操作不應該放在這兩個類中。盡量保持它們代碼的精簡,這樣你可以避免很多與生命周期相關的問題。記住你並不能掌控Activity和Fragment,他們只是在你的App和Android系統間起了橋梁的作用。任何時候,Android系統可能會根據用戶操作或其他因素(如低內存)來回收它們。最好盡量減少對他們的依賴,以提供堅實的用戶體驗。
還有一點比較重要的就是持久模型驅動UI。使用持久模型主要是因為當你的UI被回收或者在沒有網絡的情況下還能正常給用戶展示數據。模型
是用來處理應用數據的組件,它們獨立於應用中的視圖和四大組件。因此模型的生命周期必然和UI是分離的。保持UI代碼的整潔,會讓你能更容易的管理和調整UI。讓你的應用基於模型開發可以很好的管理你應用的數據並是你的應用更具測試性和持續性。
應用架構推薦
回到這篇文章的主題,來說說Android官方架構組件(一下簡稱架構)。一下會介紹如何在你的應用中實踐這一架構模式。
注意:不可能存在某一種架構方式可以完美適合任何場景。話雖如此,這種架構應該是大多數用例的良好起點。如果你已經有了很好的Android應用程序架構方式,請繼續保持。
假設我們需要一個現實用戶資料的UI,該用戶的資料文件將使用REST API從服務端獲取。
構建用戶界面
我們的這個用戶界面由一個UserProfileFragment.java
文件和它的布局文件user_profile_layout.xml
。
為了驅動UI,數據模型需要持有下面兩個數據:
- User ID:用戶的標識符。最好使用Fragment的參數將此信息傳遞到Fragment中。如果Android操作系統回收了Fragment,則會保留此信息,以便下次重新啟動應用時,該ID可用。
- User Object:傳統的Java對象,代表用戶的數據。
為此,我們新建一個繼承自ViewModel的名為UserProfileViewModel
的模型來持有這個數據。
ViewModel提供特定UI組件的數據,例如Activity和Fragment,並處理與數據處理業務部分的通信,例如調用其他組件來加載數據或轉發用戶修改。ViewModel不了解View,並且不受UI的重建(如重由於旋轉而導致的Activity的重建)的影響。
現在我們有一下三個文件:
- user_profile.xml: 視圖的布局文件。
- UserProfileViewModel.java: 持有UI數據的模型。
- UserProfileFragment.java: 用於顯示數據模型中的數據並和用戶進行交互。
一下是具體代碼(為了簡化,布局文件省略)。
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);
}
}
注意:上面的UserProfileFragment繼承自LifeCycleFragment而不是Fragment。當Lifecycle的Api穩定后,Fragment會默認實現LifeCycleOwner。
現在,我們有三個文件,我們如何連接它們?畢竟,當ViewModel的用戶字段被設置時,我們需要一種通知UI的方法。這里就要提到LiveData了。
LiveData是一個可觀察的數據持有者。它允許應用程序中的組件觀察LiveData對象持有的數據,而不會在它們之間創建顯式和剛性的依賴路徑。LiveData還尊重你的應用程序組件(Activity,Fragment,Service)的生命周期狀態,並做正確的事情以防止內存泄漏,從而你的應用程序不會消耗更多的內存。
如果你已經使用了想Rxjava活着Agrea這類第三方庫,那么你可以使用它們代替LiveData,不過你需要處理好它們與組件生命周期之間的關系。
現在我們使用LiveData<User>
來代替UserProfileViewModel中的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 -> {
// update UI
});
}
一旦用戶數據更新,onChanged回調將被調用然后UI會被刷新。
如果你熟悉一些使用觀察者模式第三方庫,你會覺得奇怪,為什么沒有在Fragment的onStop()方法中將觀察者移除。對於LiveData來說這是沒有必要的,因為它是生命周期感知的,這意味着如果UI處於不活動狀態,它就不會調用觀察者的回調來更新數據。並且在onDestroy后會自動移除。
我們也不需要處理任何視圖重建(如屏幕旋轉)。ViewModel會自動恢復重建前的數據。當新的視圖被創建出來后,它會接收到與之前相同的ViewModel實例,並且觀察者的回調會被立刻調用,更新最新的數據。這也是ViewModel為什么不能直接引用視圖對象,因為它的生命周期長於視圖對象。
獲取數據
現在我們將視圖和模型連接起來,但是模型該怎么獲取數據呢?在這個例子中,我們假設使用REST API從后台獲取。我們將使用Retrofit來向后台請求數據。
我們的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可以直接操作Webservice來獲取用戶數據。雖然這樣可以正常工作,但你的應用無法保證它的后續迭代。因為這樣做將太多的責任讓ViewModel來承擔,這樣就違反類之前講到的分層原則。又因為ViewModel的生命周期是綁定在Activity和Fragment上的,所以當UI被銷毀后如果丟失所有數據將是很差的用戶體驗。所以我們的ViewModel將和一個新的模塊進行交互,這個模塊叫Repository。
Repository模塊負責處理數據。它為應用程序的其余部分提供了一個干凈的API。他知道在數據更新時從哪里獲取數據和調用哪些API調用。你可以將它們視為不同數據源(持久性模型,Web服務,緩存等)之間的中介者。
UserRepository類如下:
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模塊看上去沒有必要,但他起着重要的作用。它為App的其他部分抽象出了數據源。現在我們的ViewModel並不知道數據是通過WebService來獲取的,這意味着我們可以隨意替換掉獲取數據的實現。
管理組件間的依賴關系
上面這種寫法可以看出來UserRepository需要初始化Webservice實例,這雖然說起來簡單,但要實現的話還需要知道Webservice的具體構造方法該如何寫。這將加大代碼的復雜度,另外UserRepository可能並不是唯一使用Webservice的對象,所以這種在內部構建Webservice實例顯然是不推薦的,下面有兩種模式來解決這個問題:
- 依賴注入:依賴注入允許類定義它們的依賴關系而不構造它們。在運行時,另一個類負責提供這些依賴關系。我們建議在Android應用程序中使用Google的Dagger 2庫實現依賴注入。Dagger 2通過遍歷依賴關系樹自動構建對象,並在依賴關系上提供編譯時保證。
- 服務定位器:服務定位器提供了一個注冊表,其中類可以獲取它們的依賴關系而不是構造它們。與依賴注入(DI)相比,實現起來相對容易,因此如果您不熟悉DI,請改用Service Locator。
這些模式允許你擴展代碼,因為它們提供明確的模式來管理依賴關系,而不會重復代碼或增加復雜性。兩者都允許交換實現進行測試;這是使用它們的主要好處之一。在這個例子中,我們將使用Dagger 2來管理依賴關系。
連接ViewModel和Repository
現在,我們的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;
}
}
緩存數據
上面的Repository雖然網絡請求做了封裝,但是它依賴后台數據源,所以存在不足。
上面的UserRepository實現的問題是,在獲取數據之后,它不會保留在任何地方。如果用戶離開UserProfileFragment並重新進來,則應用程序將重新獲取數據。這是不好的,有兩個原因:它浪費了寶貴的網絡帶寬和迫使用戶等待新的查詢完成。為了解決這個問題,我們將向我們的UserRepository添加一個新的數據源,它將把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;
}
}
持久化數據
在當前的實現中,如果用戶旋轉屏幕或離開並返回到應用程序,現有UI將立即可見,因為Repository會從內存中檢索數據。但是,如果用戶離開應用程序,並在Android操作系統殺死進程后幾小時后又會怎么樣?
在目前的實現中,我們將需要從網絡中再次獲取數據。這不僅是一個糟糕的用戶體驗,也是浪費,因為它將使用移動數據來重新獲取相同的數據。你以通過緩存Web請求來簡單地解決這個問題,但它會產生新的問題。如果請求一個朋友列表而不是單個用戶,會發生什么情況?那么你的應用程序可能會顯示不一致的數據,這是最令人困惑的用戶體驗。例如,相同的用戶的數據可能會不同,因為朋友列表請求和用戶請求可以在不同的時間執行。你的應用需要合並他們,以避免顯示不一致的數據。
正確的處理方法是使用持久模型。這時候Room就派上用場了。
Room是一個對象映射庫,它提供本地數據持久性和最少的樣板代碼。在編譯時,它根據模式驗證每個查詢,從而錯誤的SQL查詢會導致編譯時錯誤,而不是運行時失敗。Room抽象了使用原始SQL表和查詢的一些基本實現細節。它還允許觀察數據庫數據(包括集合和連接查詢)的更改,通過LiveData對象公開這些更改。
要使用Room我們首先需要使用@Entity來定義實體:
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
接着創建數據庫類:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
值得注意的是MyDatabase是一個抽象了,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);
}
接着在MyDatabase中添加獲取上面這個DAO的方法:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
這里的load方法返回的是LiveData
現在我們可以修改UserRepository了:
@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的直接數據來源從Webservice改為本地數據庫,但我們卻不需要修改UserProfileViewModel或者UserProfileFragment。這就是抽象層帶來的好處。這也給測試帶來了方便,因為你可以提供一個虛假的UserRepository來測試你的UserProfileViewModel。
現在,如果用戶重新回到這個界面,他們會立刻看到數據,因為我們已經將數據做了持久化的保存。當然如果有用例需要,我們也可不展示太老舊的持久化數據。
在一些用例中,比如下拉刷新,如果正處於網絡請求中,那UI需要告訴用戶正處於網絡請求中。一個好的實踐方式就是將UI與數據分離,因為UI可能因為各種原因被更新。從UI的角度來說,請求中的數據和本地數據類似,只是它還沒有被持久化到數據庫中。
以下有兩種解決方法:
- 將getUser的返回值中加入網絡狀態。
- 在Repository中提供一個可以返回刷新狀態的方法。如果你只是想在用戶通過下拉刷新來告訴用戶目前的網絡狀態的話,那這個方法是比較適合的。
數據唯一來源
在以上實例中,數據唯一來源是數據庫,這樣做的好處是用戶可以基於穩定的數據庫數據來更新頁面,而不需要處理大量的網絡請求狀態。數據庫有數據則使用,沒有數據則等待其更新。
測試
我們之前提到分層可以個應用提供良好的測試能力,接下來就看看我們怎么測試不同的模塊。
- 用戶界面與交互:這是唯一一個需要使用到
Android UI Instrumentation test
的測試模塊。測試UI的最好方法就是使用Espresso框架。你可以創建Fragment然后提供一個虛假的ViewModel。因為Fragment只跟ViewModel交互,所以虛擬一個ViewModel就足夠了。 - ViewModel:ViewModel可以用JUnit test進行測試。因為其不涉及界面與交互。而且你只需要虛擬UserRepository即可。
- UserRepository:測試UserRepository同樣使用JUnit test。你可以虛擬出Webservice和DAO。你可以通過使用正確的網絡請求來請求數據,讓后將數據通過DAO寫入數據庫。如果數據庫中有相關數據則無需進行網絡請求。
- UserDao:對於DAO的測試,推薦使用instrumentation進行測試。因為此處無需UI,並且可以使用in-memory數據庫來保證測試的封閉性,不會影響到磁盤上的數據庫。
- Webservice:保持測試的封閉性是相當重要的,因此即使是你的Webservice測試也應避免對后端進行網絡呼叫。有很多第三方庫提供這方面的支持。例如,MockWebServer是一個很棒的庫,可以幫助你為你的測試創建一個假的本地服務器。
架構圖
指導原則
編程是一個創意領域,構建Android應用程序也不例外。有多種方法來解決問題,無論是在多個Activity或Fragment之間傳遞數據,還是檢索遠程數據並將其在本地保持離線模式,或者是任何其他常見的場景。
雖然以下建議不是強制性的,但經驗告訴我們,遵循這些建議將使你的代碼庫從長遠來看更加強大,可測試和可維護。
- 在AndroidManifest中定義的Activity,Service,Broadcast Receiver等,它們不是數據源。相反,他們只是用於協調和展示數據。由於每個應用程序組件的壽命相當短,運行狀態取決於用戶與其設備的交互以及運行時的整體當前運行狀況,所以不要將這些組件作為數據源。
- 你需要在應用程序的各個模塊之間創建明確界定的責任范圍。例如,不要在不同的類或包之間傳遞用於加載網絡數據的代碼。同樣,不要將數據緩存和數據綁定這兩個責任完全不同的放在同一個類中。
- 每個模塊之間要竟可能少的相互暴露。不要抱有僥幸心理去公開一個關於模塊的內部實現細節的接口。你可能會在短期內獲得到便捷,但是隨着代碼庫的發展,你將多付多次技術性債務。
- 當你定義模塊之間的交互時,請考慮如何使每個模塊隔離。例如,擁有用於從網絡中提取數據的定義良好的API將使得更容易測試在本地數據庫中持久存在該數據的模塊。相反,如果將這兩個模塊的邏輯組合在一起,或者將整個代碼庫中的網絡代碼放在一起,那么測試就更難(如果不是不可能)。
- 你的應用程序的核心是什么讓它獨立出來。不要花時間重復輪子或一次又一次地編寫相同的樣板代碼。相反,將精力集中在使你的應用程序獨一無二的同時,讓Android架構組件和其他推薦的庫來處理重復的樣板代碼。
- 保持盡可能多的相關聯的新鮮數據,以便你的應用程序在設備處於脫機模式時可用。雖然你可以享受恆定和高速連接,但你的用戶可能不會。
- 你的Repository應指定一個數據源作為真實的單一來源。每當你的應用程序需要訪問這些數據時,它應該始終源於真實的單一來源。
擴展: 公開網絡狀態
在上面的小結我們故意省略了網絡錯誤和加載狀態來保證例子的簡潔性。在這一小結我們演示一種使用Resource類來封裝數據及其狀態。以此來公開網絡狀態。
下面是簡單的Resource實現:
//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);
}
}
以為從網絡上抓取視頻的同時在UI上顯示數據庫的舊數據是很常見的用例,所以我們要創建一個可以在多個地方重復使用的幫助類NetworkBoundResource。以下是NetworkBoundResource的決策樹:
NetworkBoundResource從觀察數據庫開始,當第一次從數據庫加載完實體后,NetworkBoundResource會檢查這個結果是否滿足用來展示的需求,如不滿足則需要從網上重新獲取。當然以上兩種情況可能同時發生,你希望先將數據顯示在UI上的同時去網絡上請求新數據。
如果網絡請求成果,則將結果保存到數據庫,然后重新從數據庫加載數據,如果網絡請求失敗,則直接傳遞錯誤信息。
注意:在上面的過程中可以看到當將新數據保存到數據庫后,我們重新從數據庫加載數據。雖然大部分情況我們不必如此,因為數據庫會為我們傳遞此次更新。但另一方面,依賴數據庫內部的更新機制並不是我們想要的如果更新的數據與舊數據一致,則數據谷不會做出更新提示。我們也不希望直接從網絡請求中獲取數據直接用於UI,因為這樣違背了單一數據源的原則。
下面是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;
}
}
注意到上面定義了兩種泛型,ResultType和RequestType,因為從網絡請求返回的數據類型可能會和數據庫返回的不一致。
另外注意到上面代碼中的ApiResponse這個類,他是將Retroft2.Call轉換成LiveData的一個簡單封裝。
下面是NetworkBoundResource余下部分的實現:
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
//1初始化NetworkBoundResource
result.setValue(Resource.loading(null));
//2從數據庫加載本地數據
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
//3加載完成后判斷是否需要從網上更新數據
result.removeSource(dbSource);
if (shouldFetch(data)) {
//4從網上更新數據
fetchFromNetwork(dbSource);
} else {
//直接用本地數據更新
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
//5進行網絡請求
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()) {
//6請求數據成功,保存數據
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) {
//7保存請求到的數據
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.
//8再次加載數據庫,使用數據庫中的最新數據
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}
接着我們就可以在UserRepository中使用NetworkBoundResource了。
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();
}
}