本指南使用於具備構建應用程序基礎而且想了解構建強大、優質的應用程序的最佳實踐和推薦架構的開發人員。html
注:本指南假定讀者熟悉 Android Framework。若是你是一個應用程序開發的新手,請參閱入門指南系列培訓,其中包含了本指南先決條件的相關主題。java
在大多數狀況下,桌面應用程序在啓動器快捷方式中有一個單一的入口而且做爲單獨的獨立進程運行,與桌面應用程序不一樣,Android 應用具備更復雜的結構。一個典型的 Android 應用是由多個應用程序組件構成的,包括 activity,fragment,service,content provider 和 broadcast receiver。react
這些應用程序組件中的大部分聲明在由 Android OS 使用的應用程序清單中,用來決定如何將應用融入到用戶設備的總體體驗中。儘管如前所述,傳統的桌面應用程序做爲獨立進程運行,可是正確的編寫 Android 應用程序須要更加靈活,由於用戶會同過設備上不一樣的應用程序組織成本身的方式不斷切換流程和任務。android
例如,考慮下在你喜歡的社交網絡應用中分享照片時會發生什麼。該應用會觸發一個啓動相機的 intent,從該 intent 中 Android OS 會啓動一個相機應用來處理這個請求。在此刻,用戶離開社交網絡應用可是用戶的體驗是無縫的。相機應用轉而可能會觸發其它的 intent,例如啓動文件選擇器,這可能會啓動另外一個應用。最終用戶回到社交網絡應用而且分享照片。此外,在這個過程當中的任什麼時候刻用戶都有可能會被一個電話打斷,而且在結束通話後再回來繼續分享照片。git
在 Android 中,這種應用切換行爲很常見,因此你的應用程序必須正確處理這些流程。記住,移動設備的資源是有限的,因此在任什麼時候候,操做系統均可能會殺死一些應用爲新的應用騰出空間。github
其中的重點是應用程序組件可能會被單獨和無序的啓動,而且可能會被用戶或系統在任什麼時候候銷燬。由於應用程序組件是短暫的,而且其聲明週期(何時被建立和銷燬)不受你控制,因此不該該在應用程序組件中存儲任何應用數據或狀態,同時應用程序組件不該該相互依賴。web
若是不能在應用程序組件中存儲應用數據和狀態,那麼應該如何構建應用?數據庫
最重要的是在應用中要專一於關注點分離。一個常見的錯誤是在 Activity 或 Fragment 中編寫全部的代碼。任何不是處理 UI 或 操做系統交互的代碼都不該該在這些類中。保持它們儘量的精簡能夠避免許多與生命週期有關的問題。不要忘記你不擁有這些類,它們只是體現了 OS 和 應用之間協議的粘合類。Android OS 可能會由於用戶交互或其餘因素(如低內存)的緣由在任什麼時候候銷燬它們。最好儘可能減小對它們的依賴以提供一個穩固的用戶體驗。編程
第二個重要的原則是應該用 Model 驅動 UI,最好是持久化的 Model。持久化是最佳的緣由有兩個:一是若是 OS 銷燬應用釋放資源,用戶不用擔憂丟失數據;二是即便網絡鏈接不可靠或者是斷開的,應用仍將繼續運行。Model 是負責處理應用數據的組件。Modle 獨立於應用中的 View 和應用程序組件,所以 Model 和這些組件的生命週期問題隔離開了。保持 UI 代碼精簡而且摒除應用的邏輯使其更易於管理。基於 Model 類構建的應用程序其管理數據的職責明確,使應用程序可測試而且穩定。後端
在本節中,咱們將經過一個用例來演示如何使用 Architecture Components 構建應用程序。
注:不可能有一種應用程序的編寫方式對於每種狀況都是最好的。話雖如此,這個推薦的架構應該是大多數用例的良好起點。若是你已經有一種很好的應用程序編寫方式則不須要改變。
假設咱們正在構建一個顯示用戶我的信息的 UI。用戶的我的信息將使用 REST API 從咱們本身的私有後端獲取。
UI 包含一個 fragment 文件 UserProfileFragment.java 和其佈局文件 user_profile_layout.xml。
爲了驅動 UI,數據模型須要持有兩個數據元素。
用戶 ID :用戶的標識符。最好使用 fragment 的參數將用戶 ID 傳到 fragment 中。若是 Android OS 銷燬進程,該 ID 將會被保存,以便下次應用重啓時該 ID 可用。
用戶對象:保存用戶數據的普通 Java 對象(POJO)。
咱們將會基於 ViewModel 來建立一個 UserProfileViewModel 來保存這些信息。
ViewModel 爲指定的 UI 組件(如:fragment 或 activity)提供數據,而且負責與數據處理的業務部分的交互,例如:調用其它組件獲取數據或轉發用戶的操做。ViewModel 對於 View 並不瞭解而且不受配置改變(如:因爲旋轉致使 activity 的從新建立)的影響
如今有 3 個文件
user_profile.xml:屏幕上的 UI 定義。
UserProfileViewModel.java:爲 UI 準備數據的類。
UserProfileFragment.java:用於在 ViewModel 中顯示數據並對用戶交互作出反應的 UI 控制器。
下面是咱們的初始實現(簡單起見省略佈局文件):
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 類。在 Architecture Components 中的生命週期 API 穩定後, Android 支持包中的 Fragment 類將會實現 LifecycleOwner 接口。
如今咱們有了 3 個代碼模塊,怎樣鏈接它們?最後,當 ViewModel 的用戶字段被設置時,須要一種方式來通知 UI。這正是 LiveData 的用武之地。
LiveData 是一個可觀察的數據持有者。它容許應用程序中的組件觀察 LiveData 進行改變,而不會在組件之間建立顯示的,固定的依賴。另外,LiveData 還遵照應用程序組件(如:activity,fragment,service)的生命週期狀態,而且防止對象泄漏使應用不會消耗更多的內存。
注:若是你已經再使用像 RxJava 或 Agera 的庫,你能夠繼續使用它們而不用換成 LiveData。可是當使用它們或其它的方式時,請確保正確處理生命週期,如:當相關 LifecycleOwner 中止時暫停數據流或在 LifecycleOwner 被銷燬時銷燬數據流。能夠添加 android.arch.lifecycle:reactivestreams 工具,和其它的響應流庫(如:RxJava2)一塊兒使用 LiveData。
將 UserProfileViewModel 中的 User 字段替換爲 LiveData
public class UserProfileViewModel extends ViewModel {
...
// private User user;
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
});
}複製代碼
每次更新用戶數據時,將會調用 onChange 回調而且更新 UI。
若是你熟悉其它庫的可觀察回調的使用,可能已經意識到咱們沒有重寫 fragment 的 onStop() 方法來中止觀察數據。這對於 LiveData 來講是沒必要要的,由於 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 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 的生命週期,所以在 Activity 或 Fragment 的生命週期結束時丟失全部的數據是一種很差的用戶體驗。故而,咱們的 ViewModel 將把這項工做委託給一個新的 Repository 模塊。
Repository 模塊負責處理數據操做。它們爲應用程序的其它部分提供了一個乾淨的 API。它們知道在數據更新時從哪裏獲取數據和調用哪些 API。能夠將其視爲不一樣數據源(持久化模型,Web 服務,緩存等)之間的中間層。
下面的 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;
}
}複製代碼
雖然 Repository 模塊看起來是沒必要要的,可是它起着一個重要的做用;它抽象了應用程序其它部分的數據源。如今 ViewModel 不知道數據是由 Webservice 獲取的,這意味着能夠根據需求將其切換爲其它實現。
注:爲了簡單起見,咱們忽略了網絡錯誤的狀況。有關於暴露錯誤和加載狀態的可選實現方式,請參閱附錄:暴露網絡狀態。
上面的 UserRepository 類須要一個 Webservice 的實例來完成其工做。能夠簡單的建立 Webservice,可是這須要知道 Webservice 的依賴來構造它。這將會顯著的使代碼複雜和重複(例如:須要 Webservice 實例的每一個類都須要知道如何使用它的依賴來構造它)。另外,UserRepostory 可能不是惟一須要 Webservice 的類。若是每一個類都建立一個新的 Webservice,這將會形成很是大的資源負擔。
有兩種模式能夠解決這個問題:
依賴注入:依賴注入容許類定義其依賴而不構造它們。在運行時,另外一個類負責提供這些依賴。推薦使用 Google 的 Dagger 2 庫在 Android 應用中實現依賴注入。Dagger 2 經過遍歷依賴關係樹自動構建對象併爲依賴提供編譯時保障。
服務定位:服務定位提供了一個註冊表,類能夠從中獲取它們的依賴關係,而不是構造它們。與依賴注入(DI)相比,服務定位實現起來相對容易,因此若是不熟悉 DI,請使用服務定位代替。
這些模式容許你擴展代碼,由於它們提供清晰的模式來管理依賴關係,而不會重複代碼或增長複雜性。二者都容許替換實現進行測試;這是使用它們的主要好處之一。
在這個例子中,咱們將使用 Dagger 2 來管理依賴。
修改 UserProfileViewModel 以使用 Repository。
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 建立的
// 因此咱們知道 userId 不會改變
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}複製代碼
上述 Repository 的實現對於抽象調用 Web 服務是很好的,可是由於它僅依賴於一個數據源因此不是很實用。
上述 UserRepository 的實現的問題是在獲取到數據以後沒有把數據保存下來。若是用戶離開 UserProfileFragment 而後返回回來,應用將會從新獲取數據。這樣是很很差的,緣由有兩個:一是浪費了寶貴的網絡帶寬;二是迫使用戶等待新的查詢完成。爲了解決這個問題,咱們將在 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;
}
}複製代碼
在當前的實現中,若是用戶旋轉屏幕或離開並返回應用,已存在的 UI 將會當即可見,由於 Repository 從內存緩存中取回數據。可是,若是用戶離開應用並在幾個小時後(Android OS 已經殺死進程後)返回又會發生什麼?
若是是目前的實現,將會須要再次從網絡獲取數據。這不只是一個很差的用戶體驗,而且也是浪費,由於這將會使用移動數據從新獲取一樣的數據。能夠經過緩存 Web 請求來簡單的解決這個問題,可是這會致使新的問題。若是相同的用戶數據來自另外一種請求並顯示(例如:獲取朋友列表)將會怎樣?這時應用會顯示不一致的數據,這是最使人困惑的用戶體驗。例如:相同用戶的數據可能會顯示的不一樣,由於朋友列表的請求和用戶我的信息的請求可能會在不一樣的時間執行。應用程序須要合併它們以免顯示不一致的數據。
解決這個問題的正確方法是使用持久化模型。這就是持久化庫 Room 的用武之地了。
Room 是一個以最少的樣板代碼提供本地數據持久化的對象映射庫。在編譯時,它會根據模式驗證每一個查詢,因此損壞的 SQL 查詢只會致使編譯時錯誤,而不是運行時崩潰。Room 抽象出一些使用原始 SQL 表查詢的底層實現細節。它還容許觀察數據庫數據(包括集合和鏈接查詢)的變化,並經過 LiveData 對象暴露這些變化。另外,它明肯定義了線程約束以解決常見問題(如在主線程訪問存儲)。
注:若是你熟悉其它的持久化解決方案,如:SQLite ORM 或像 Realm 等其餘的數據庫,你不須要用更換爲 Room,除非 Room 的功能對你的用例更加適用。
使用 Room,須要定義咱們的局部模式。首先,用 @Entity 註釋 User 類,將其標記爲數據庫中的一個表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// 字段的 get 和 set 方法
}複製代碼
而後,經過繼承 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
注:從 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);
// 直接從數據庫返回一個 LiveData。
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// 在後臺線程中運行
// 檢查最近是否獲取過 user
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,他們會當即看到用戶信息,由於咱們已經將其持久化了。同時,若是數據過時,Repository 將會在後臺更新數據。固然,根據你的用例,若是持久化的數據太舊你可能不但願顯示它們。
在一些用例中,以下拉刷新,在進行網絡操做時顯示用戶數據對於 UI 來講很是重要。將 UI 操做從實際數據中分離是一個很好的作法,由於其可能因爲各類緣由而被更新(例如:若是獲取一個朋友列表,可能會再次獲取到相同的 user 並觸發 LiveData
這種用例有兩種常見的解決方案:
更改 getUser 以返回包含網絡操做狀態的 LiveData。在附錄:暴露網絡狀態部分提供了一個實現例子。
在 Repository 類中提供一個能夠返回 User 刷新狀態的公共方法。若是隻是爲了響應明確的用戶操做而在 UI 中顯示網絡狀態(如:下拉刷新),則這種方式是更好的選擇。
不一樣的 REST API 接口返回相同的數據是很常見的。例如,若是後端有另外一個返回朋友列表的接口,相同的用戶對象(也許是不一樣的粒度)可能來自兩個不一樣的 API 接口。若是 UserRepository 把從 Webservice 請求獲取到的響應原樣返回,碼麼 UI 可能會顯示不一致的數據,由於在這些請求之間數據可能在服務端發生了改變。這就是爲何在 UserRepository 的實現中 Web 服務的回調只是將數據保存到了數據庫。而後,對數據庫的更改將會觸發處於活動狀態的 LiveData 對象上的回調。
在這個模型中,數據庫服務做爲單一數據源,應用程序的其它部分經過 Repository 來訪問它。不管是不是否磁盤緩存,建議 Repository 指定一個數據源做爲應用程序其它部分的單一數據源。
咱們已經提到分離的好處之一是可測試性。讓咱們看看如何測試每一個代碼模塊。
用戶界面或用戶交互:這將是惟一一次須要 Android UI Instrumentation test。測試 UI 代碼的最佳方式是建立一個 Espresso 測試。能夠建立一個 fragment 併爲其提供一個模擬的 ViewModel。由於 fragment 只和 ViewModel 交互,隨意模擬 ViewModel 足以徹底測是 UI。
ViewModel:可使用 JUnit test 來測試 ViewModel。只須要模擬 UserRepository 來測試它。
UserRepository:也可使用 JUnit test 來測試 UserRepository。須要模擬 Webservice 和 DAO。能夠測試 UserRepository 是否進行了正確的 Web 服務調用,將結構保存到數據庫,若是數據被緩存且是最新的,則不會發起任何沒必要要的請求。由於 WebService 和 UserDao 都是接口,因此能夠模擬它們或者爲更復雜的測試用例建立假的實現。
UserDao:推薦使用 Instrumentation 測試的方式測試 DAO 類。由於 Instrumentation 測試不須要任何 UI,它們會運行的很快。對於每一個測試,能夠建立一個內存數據庫以確保測試沒有任何反作用(例如:改變磁盤上的數據庫文件)。
Room 還容許指定數據庫實現,因此能夠經過向其提供 SupportSQLiteOpenHelper 的 JUnit 實現來測試它。一般不推薦這種方式,由於設備上運行的 SQLite 版本可能和主機上的 SQLite 版本不一樣。
Webservice:重點是使測試相對於外部獨立,因此 Webservice 的測試要避免經過網絡調用後端。有許多庫能夠幫助完成該測試。例如:MockWebServer 是一個很好的庫,能夠幫助爲測試建立一個假的本地服務。
測試工件架構組件提供了一個 maven 工件來控制其後臺線程。在 android.arch.core:core-testing 工件中,有兩個 JUnit 規則:
InstantTaskExecutorRule:該規則可用於強制架構組件當即執行調用線程上的任何後臺操做。
CountingTaskExecutorRule:該規則可用於 Instrumentation 測試,以等待架構組件的後臺操做,或將其鏈接到 Espresso 做爲閒置資源。
下圖顯示了推薦架構中的全部模塊以及它們如何互相交互:
編程是一個創做領域,構建 Android 應用也不例外。有許多方法來解決問題,不管是在多個 activity 或 fragment 之間傳遞數據,是獲取遠程數據併爲了離線模式將其持久化到本地,仍是特殊應用遭遇的其它常見狀況。
雖然一下建議不是強制性的,可是以咱們的經驗,從長遠來看,遵循這些建議將會使代碼庫更健壯,易測試和易維護。
在 manifest 中定義的入口點,如:acitivy,fragment,broadcast receiver 等,不是數據源。相反,它們應該只是協調與該入口點相關的數據子集。因爲每一個應用程序組件的存活時間很短,這取決於用戶與其設備的交互以及運行時的整體情況,因此任何入口點都不該該成爲數據源。
嚴格的在應用程序的各個模塊之間建立明確的責任界限。例如:不在代碼庫中的多個類或包中擴散從網絡加載數據的代碼。一樣,不要將無關的責任(如:數據緩存和數據綁定)放到同一個類中。
每一個模塊儘量少的暴露出來。不要視圖建立暴露模塊內部實現細節的「只一個」的快捷方式。你可能會在短時間內節省一些時間,可是隨着代碼庫的發展,你將會屢次償還更多的基數債務。
當定義模塊間的交互時,請考慮如何讓每一個模塊能夠獨立的測試。例如,擁有一個用於從網絡獲取數據且定義良好的 API 的模塊,將會使其更易於測試在本地數據庫中持久化數據。相反,若是將兩個模塊的邏輯放在一個地方,或者將網絡代碼擴散到整個代碼庫,測試將會變的很是困難(並不是不可能)。
應用程序的核心是使其脫穎而出。不要花費時間重複造輪子或一次又一次的編寫相同的樣板代碼。相反,將精力集中在使應用程序獨一無二上,讓 Android Architecture Components 和其它的優秀的庫來處理重複的樣板代碼。
持久化儘量多的相關最新數據,以便應用程序在設備處於離線模式時還可使用。即便你能夠享用穩定高速的網絡鏈接,可是你的用戶可能沒法享用。
Repository 應該指定一個數據源做爲單一數據源。每當應用程序須要訪問數據時,數據應該始終來源於單一數據源。有關更多信息,請參閱單一數據源
在上面推薦的應用程序架構部分,爲了保持示例簡單咱們故意忽略網絡錯誤和加載狀態。在本節中,咱們演示一種經過 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 檢查返回結果是否足夠好能夠被髮送和(或)應該從網絡獲取數據。請注意,他們可能同時發生,由於你可能會但願在顯示緩存數據的同時從網絡更新數據。
若是網絡調用成功,則將返回數據保存到數據庫中並從新初始化數據流。若是網絡請求失敗,直接發送一個錯誤。
注:將新的數據保存到磁盤後,要從數據庫從新初始化數據流,可是一般不須要這樣作,由於數據庫將會發送變動。另外一方面,依賴數據庫發送變動會有一些很差的反作用,由於在數據沒有變化時若是數據庫會避免發送更改將會使其中斷。咱們也不但願發送從網絡返回的結果,由於這違背的單一數據源原則(即便在數據庫中有觸發器會改變保存值)。咱們也不但願在沒有新數據的時候發送 SUCCESS,由於這會給客戶端發送錯誤信息。
如下是 NetworkBoundResource 類爲其子類提供的公共 API:
// ResultType: Resource 數據的類型
// 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() {
}
// 返回一個表明 Resource 的 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);
//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) {
// 指定請求一個最新的實時數據。
// 不然,會獲得最新的緩存數據,而且可能不會由從網絡獲取的最新數據更新。
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}複製代碼
如今,可使用 NetworkBoundResource 在 Repository 中編寫磁盤和網絡綁定 User 的實現。
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();
}
}複製代碼