google官方地址:https://developer.android.com/jetpack/docs/guide#addendum
應用程序架構指南
本指南適用於過去構建應用程序基礎知識的開發人員,現在希望瞭解構建強大的,生產質量的應用程序的最佳實踐和建議的體系結構。
本頁假定您熟悉Android Framework。如果您不熟悉應用程序開發,請查看入門培訓系列,其中包含本指南的先決條件主題。
應用開發者面臨的常見問題
與傳統的桌面版本不同,在大多數情況下,它們只有一個來自啓動器快捷方式的入口點並作爲單個整體流程運行,Android應用程序的結構要複雜得多。典型的Android應用程序由多個應用程序組件構成,包括活動,片段,服務,內容提供程序和廣播接收器。
大多數這些應用程序組件都在應用程序清單中聲明,Android操作系統使用該清單來決定如何將您的應用程序集成到其設備的整體用戶體驗中。雖然如前所述,桌面應用程序傳統上是一個單一的流程,但是正確編寫的Android應用程序需要更加靈活,因爲用戶在設備上編寫不同的應用程序,不斷切換流程和任務。
例如,考慮當您在自己喜歡的社交網絡應用中分享照片時會發生什麼。該應用程序觸發相機意圖,Android操作系統啓動相機應用程序來處理請求。此時,用戶離開社交網絡應用程序,但他們的體驗是無縫的。反過來,相機應用程序可能會觸發其他意圖,例如啓動文件選擇器,這可能會啓動另一個應用程序。最終用戶回到社交網絡應用程序並共享照片。此外,在此過程中的任何時刻,用戶都可能被電話打斷,並在完成電話呼叫後回來分享照片。
在Android中,這種跳頻行爲很常見,因此您的應用必須正確處理這些流量。請記住,移動設備受資源限制,因此在任何時候,操作系統都可能需要殺死某些應用程序以爲新設備騰出空間。
所有這一切的重點在於,您的應用程序組件可以單獨啓動,也可以無序啓動,並且可以由用戶或系統隨時銷燬。由於應用程序組件是短暫的,並且它們的生命週期(當它們被創建和銷燬時)不在您的控制之下,因此您不應在應用程序組件中存儲任何應用程序數據或狀態,並且您的應用程序組件不應相互依賴。
共同的建築原則
如果您無法使用應用程序組件來存儲應用程序數據和狀態,那麼應該如何構建應用程序?
您應該關注的最重要的事情是在您的應用中分離關注點。在a Activity
或a中編寫所有代碼是一個常見的錯誤Fragment
。任何不處理UI或操作系統交互的代碼都不應該在這些類中。儘可能保持精簡可以避免許多與生命週期相關的問題。不要忘了,你不擁有這些類,它們是體現了OS和您的應用程序之間的合同只是膠類。Android操作系統可能會根據用戶交互或其他因素(如內存不足)隨時銷燬它們。最好儘量減少對它們的依賴,以提供可靠的用戶體驗。
第二個重要原則是您應該從模型中驅動UI,最好是持久模型。持久性是理想的兩個原因:如果操作系統破壞您的應用程序以釋放資源,您的用戶將不會丟失數據,即使網絡連接不穩定或未連接,您的應用程序也將繼續工作。模型是負責處理應用程序數據的組件。它們獨立於應用程序中的視圖和應用程序組件,因此它們與這些組件的生命週期問題隔離開來。保持UI代碼簡單且沒有應用程序邏輯,使其更易於管理。將您的應用程序基於具有明確定義的數據管理職責的模型類,將使其可測試且您的應用程序保持一致。
推薦的應用架構
在本節中,我們將演示如何使用Architecture Components通過用例來構建應用程序。
注意:沒有一種方法可以編寫最適合每種情況的應用程序。話雖這麼說,這個推薦的架構應該是大多數用例的一個很好的起點。如果您已經有了編寫Android應用程序的好方法,則無需更改。
想象一下,我們正在構建一個顯示用戶配置文件的UI。此用戶配置文件將使用REST API從我們自己的私有後端獲取。
構建用戶界面
UI將包含一個片段UserProfileFragment.java
及其相應的佈局文件user_profile_layout.xml
。
爲了驅動UI,我們的數據模型需要包含兩個數據元素。
- 用戶ID:用戶的標識符。最好使用片段參數將此信息傳遞到片段中。如果Android操作系統破壞了您的進程,則會保留此信息,以便下次重新啓動應用時ID可用。
- User對象:保存用戶數據的POJO。
我們將創建一個UserProfileViewModel
基於ViewModel類來保存此信息。
甲視圖模型提供了一個特定的UI組件中的數據,如一個片段或活性,和處理與數據處理的部分業務,如主叫其他組件加載數據或轉發的用戶修改的通信。ViewModel不瞭解View,也不受配置更改的影響,例如由於輪換而重新創建活動。
現在我們有3個文件。
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 Fragment {
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);
}
}
現在,我們有這三個代碼模塊,我們如何連接它們?畢竟,當設置ViewModel的用戶字段時,我們需要一種方法來通知UI。這是LiveData類的用武之地。
LiveData是一個可觀察的數據持有者。它允許應用程序中的組件觀察
LiveData
對象以進行更改,而無需在它們之間創建明確且嚴格的依賴路徑。LiveData還尊重應用程序組件(活動,片段,服務)的生命週期狀態,並做正確的事情來防止對象泄漏,以便您的應用程序不會消耗更多內存。
注意:如果您已經在使用像RxJava或 Agera這樣的庫 ,則可以繼續使用它們而不是LiveData。但是當您使用它們或其他方法時,請確保正確處理生命週期,以便在相關LifecycleOwner停止時數據流暫停,並在銷燬LifecycleOwner時銷燬流。您還可以添加 android.arch.lifecycle:reactivestreams
工件以將LiveData與另一個反應流庫(例如,RxJava2)一起使用。
現在我們將a中的User
字段替換爲UserProfileViewModel
a, LiveData<User>
以便在更新數據時通知片段。最棒的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。
如果您熟悉使用可觀察回調的其他庫,您可能已經意識到我們不必覆蓋片段的onStop()
方法來停止觀察數據。LiveData不需要這樣做,因爲它可以識別生命週期,這意味着除非片段處於活動狀態(已接收onStart()
但未接收onStop()
),否則它不會調用回調。片段收到時,LiveData也會自動刪除觀察者onDestroy()
。
我們也沒有做任何特殊處理配置更改(例如,用戶旋轉屏幕)。ViewModel會在配置更改時自動恢復,因此只要新片段生效,它就會收到相同的ViewModel實例,並且會立即使用當前數據調用回調。這就是ViewModels不應該直接引用Views的原因; 它們可以比View的生命週期更長久。請參閱 ViewModel的生命週期。
獲取數據
現在我們將ViewModel連接到片段,但ViewModel如何獲取用戶數據?在此示例中,我們假設我們的後端提供REST API。我們將使用 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類負有太多責任,這違背了我們之前提到的關注點分離原則。另外,視圖模型的範圍是依賴於一個Activity
或Fragment
生命週期,所以失去所有,當其生命週期結束後的數據是一個不好的用戶體驗。相反,我們的ViewModel會將此工作委託給新的Repository模塊。
存儲庫模塊負責處理數據操作。它們爲應用程序的其餘部分提供了一個乾淨的API。他們知道從何處獲取數據以及在更新數據時要進行的API調用。您可以將它們視爲不同數據源(持久模型,Web服務,緩存等)之間的中介。
UserRepository
下面的類使用WebService
提取用戶數據項。
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;
}
}
即使存儲庫模塊看起來不必要,它也是一個重要的目的; 它從應用程序的其餘部分抽象出數據源。現在我們的ViewModel不知道數據是由Webservice
它獲取的,這意味着我們可以根據需要將其交換爲其他實現。
注意:爲簡單起見,我們省略了網絡錯誤情況。有關公開錯誤和加載狀態的替代實現,請參閱 附錄:公開網絡狀態。
管理組件之間的依賴關係:
UserRepository
上面的類需要一個實例Webservice
來完成它的工作。它可以簡單地創建它,但要做到這一點,它還需要知道Webservice
類的依賴關係來構造它。這將使代碼顯着複雜化和複製(例如,需要Webservice
實例的每個類 都需要知道如何使用其依賴項來構造它)。另外,UserRepository
可能不是唯一需要的課程Webservice
。如果每個類都創建一個新類WebService
,那麼它將非常耗費資源。
您可以使用兩種模式來解決此問題:
- 依賴注入:依賴注入允許類在不構造它們的情況下定義它們的依賴關係。在運行時,另一個類負責提供這些依賴項。我們建議使用Google的Dagger 2庫來實現Android應用中的依賴注入。Dagger 2通過遍歷依賴關係樹自動構造對象,併爲依賴關係提供編譯時保證。
- 服務定位器:服務定位器提供了一個註冊表,其中類可以獲取它們的依賴關係而不是構造它們。它比依賴注入(DI)更容易實現,因此如果您不熟悉DI,請使用服務定位器。
這些模式允許您擴展代碼,因爲它們提供了清晰的模式來管理依賴項,而無需複製代碼或增加複雜性。它們都允許交換實現進行測試; 這是使用它們的主要好處之一。
在此示例中,我們將使用Dagger 2來管理依賴項。
連接ViewModel和存儲庫
現在我們修改我們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;
}
}
緩存數據
上面的存儲庫實現適用於抽象Web服務的調用,但因爲它只依賴於一個數據源,所以它不是很有用。
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將立即可見,因爲存儲庫從內存緩存中檢索數據。但是,如果用戶離開應用程序並在Android操作系統殺死進程後數小時後回來會發生什麼?
通過當前的實現,我們需要從網絡中再次獲取數據。這不僅是一種糟糕的用戶體驗,而且還浪費,因爲它將使用移動數據重新獲取相同的數據。您可以通過緩存Web請求來解決此問題,但這會產生新問題。如果相同的用戶數據顯示來自另一種類型的請求(例如,獲取朋友列表)會發生什麼?然後,您的應用可能會顯示不一致的數據,這充其量只是令人困惑的用戶體驗。例如,相同用戶的數據可能不同地顯示,因爲可以在不同時間執行朋友列表請求和用戶請求。您的應用需要合併它們以避免顯示不一致的數據。
處理此問題的正確方法是使用持久模型。這是 Room persistence library來救援的地方。
Room是一個對象映射庫,它提供本地數據持久性和最少的樣板代碼。在編譯時,它根據模式驗證每個查詢,以便損壞的SQL查詢導致編譯時錯誤而不是運行時失敗。Room抽象了使用原始SQL表和查詢的一些底層實現細節。它還允許觀察對數據庫數據(包括集合和連接查詢)的更改,通過LiveData對象公開此類更改 。此外,它還明確定義瞭解決常見問題的線程約束,例如訪問主線程上的存儲。
注意:如果您的應用已使用其他持久性解決方案(如SQLite對象關係映射(ORM)),則無需使用Room替換現有解決方案。但是,如果您正在編寫新應用或重構現有應用,我們建議您使用Room來保留應用的數據。這樣,您就可以利用庫的抽象和查詢驗證功能。
要使用Room,我們需要定義本地模式。首先,註釋User
該類以@Entity
將其標記爲數據庫中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然後,通過擴展RoomDatabase
您的應用程序來創建數據庫類 :
@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);
}
然後,從我們的數據庫類中引用DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
請注意,該load
方法返回一個LiveData<User>
。會議室知道數據庫何時被修改,並且在數據發生變化時會自動通知所有活動的觀察者。因爲它使用LiveData,所以這將是有效的,因爲只有在至少有一個活動觀察者的情況下它纔會更新數據。
注意:會議室根據表格修改檢查失效,這意味着它可能會發送誤報通知。
現在我們可以修改我們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 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
,我們也不需要更改我們的UserProfileViewModel
或UserProfileFragment
。這是抽象提供的靈活性。這對於測試也很有用,因爲你可以UserRepository
在測試時提供假的UserProfileViewModel
。
現在我們的代碼已經完成。如果用戶幾天後回到相同的用戶界面,他們會立即看到用戶信息,因爲我們已經保留了用戶信息。同時,如果數據陳舊,我們的存儲庫將在後臺更新數據。當然,根據您的使用情況,如果持久數據太舊,您可能不希望顯示這些數據。
在某些用例中,例如pull-to-refresh,如果當前正在進行網絡操作,則UI必須向用戶顯示。將UI操作與實際數據分開是一種很好的做法,因爲它可能由於各種原因而更新(例如,如果我們獲取朋友列表,則可能再次獲取相同的用戶來觸發LiveData<User>
更新)。從UI的角度來看,飛行中有請求的事實只是另一個數據點,類似於任何其他數據(如User
對象)。
這個用例有兩種常見的解決方案:
- 更改
getUser
以返回包含網絡操作狀態的LiveData。附錄中提供了一個示例實現:公開網絡狀態部分。 - 在存儲庫類中提供另一個可以返回用戶刷新狀態的公共函數。如果要僅在響應顯式用戶操作(如pull-to-refresh)時在UI中顯示網絡狀態,則此選項更好。
單一的事實來源
不同的REST API端點通常會返回相同的數據。例如,如果我們的後端有另一個返回朋友列表的端點,則同一個用戶對象可能來自兩個不同的API端點,可能是不同的粒度。如果要按原樣UserRepository
返回Webservice
請求的響應,我們的UI可能會顯示不一致的數據,因爲這些請求之間的服務器端數據可能會發生變化。這就是爲什麼在UserRepository
實現中,Web服務回調只是將數據保存到數據庫中。然後,對數據庫的更改將觸發活動LiveData對象的回調。
在此模型中,數據庫充當單一事實來源,應用程序的其他部分通過存儲庫訪問它。無論您是否使用磁盤緩存,我們都建議您的存儲庫將數據源指定爲應用程序其餘部分的唯一真實來源。
測試
我們已經提到分離的一個好處是可測試性。讓我們看看我們如何測試每個代碼模塊。
-
用戶界面和交互:這是您唯一需要 Android UI Instrumentation測試的時間。測試UI代碼的最佳方法是創建 Espresso測試。您可以創建片段併爲其提供模擬ViewModel。由於片段只與ViewModel對話,因此模擬它就足以完全測試此UI。
-
ViewModel:可以使用JUnit測試來測試ViewModel 。你只需要模擬
UserRepository
它來測試它。 -
UserRepository:您也可以
UserRepository
使用JUnit測試進行測試。你需要嘲笑Webservice
和DAO。您可以測試它是否進行了正確的Web服務調用,將結果保存到數據庫中,並且如果數據被緩存並且是最新的,則不會發出任何不必要的請求。由於這兩個Webservice
和UserDao
的界面,你可以嘲笑他們或創建更復雜的測試案例假冒實現.. -
UserDao:測試DAO類的推薦方法是使用檢測測試。由於這些檢測測試不需要任何UI,因此它們仍然可以快速運行。對於每個測試,您可以創建內存數據庫以確保測試沒有任何副作用(例如更改磁盤上的數據庫文件)。
Room還允許指定數據庫實現,因此您可以通過提供它的JUnit實現來測試它
SupportSQLiteOpenHelper
。通常不建議使用此方法,因爲設備上運行的SQLite版本可能與主機上的SQLite版本不同。 -
Web服務:讓測試獨立於外部世界非常重要,因此即使您的
Webservice
測試也應避免對後端進行網絡調用。有很多庫可以幫助解決這個問題。例如, MockWebServer 是一個很棒的庫,可以幫助您爲測試創建虛假的本地服務器。 -
測試工件架構組件提供了一個maven工件來控制其後臺線程。在
android.arch.core:core-testing
工件內部 ,有2個JUnit規則:InstantTaskExecutorRule
:此規則可用於強制體系結構組件立即在調用線程上執行任何後臺操作。CountingTaskExecutorRule
:此規則可用於檢測測試,以等待架構組件的後臺操作或將其作爲空閒資源連接到Espresso。
最終的架構
下圖顯示了我們推薦的體系結構中的所有模塊以及它們如何相互交互:
指導原則
編程是一個創造性的領域,構建Android應用程序也不例外。有許多方法可以解決問題,無論是在多個活動或片段之間傳遞數據,檢索遠程數據並在本地持久保存以用於脫機模式,還是任何其他非常重要的應用程序遇到的常見場景。
雖然以下建議不是強制性的,但我們的經驗是,遵循它們將使您的代碼庫從長遠來看更加強大,可測試和可維護。
- 您在清單中定義的入口點 - 活動,服務,廣播接收器等 - 不是數據源。相反,它們應該只協調與該入口點相關的數據子集。由於每個應用程序組件都很短暫,這取決於用戶與其設備的交互以及運行時的總體當前運行狀況,因此您不希望任何這些入口點成爲數據源。
- 無情地在應用程序的各個模塊之間創建明確定義的責任範圍。例如,不要將代碼庫中的數據加載到代碼庫中的多個類或包中。同樣,不要將不相關的職責(例如數據緩存和數據綁定)填充到同一個類中。
- 從每個模塊儘可能少地暴露。不要試圖創建“只是一個”的快捷方式,從一個模塊公開內部實現細節。您可能會在短期內獲得一些時間,但隨着代碼庫的發展,您將多次支付技術債務。
- 在定義模塊之間的交互時,請考慮如何使每個模塊獨立可測試。例如,使用定義良好的API從網絡獲取數據將使測試在本地數據庫中持久存儲該數據的模塊變得更加容易。相反,如果您將這兩個模塊的邏輯混合在一個地方,或者將您的網絡代碼灑在整個代碼庫中,那麼測試將會更加困難 - 如果不是不可能的話。
- 您的應用程序的核心是讓它脫穎而出的原因。不要花時間重新發明輪子或一次又一次地編寫相同的樣板代碼。相反,將精力集中在使應用程序獨一無二的地方,讓Android架構組件和其他推薦的庫處理重複的樣板。
- 保留儘可能多的相關和新鮮數據,以便在設備處於脫機模式時您的應用程序可用。雖然您可以享受恆定和高速連接,但您的用戶可能不會。
- 您的存儲庫應將一個數據源指定爲單一事實來源。每當您的應用需要訪問此數據時,它應始終源於單一的事實來源。有關更多信息,請參閱單一事實來源。
附錄:暴露網絡狀態
在上面推薦的應用程序架構部分中,我們故意省略網絡錯誤和加載狀態以保持樣本簡單。在本節中,我們演示了一種使用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);
}
}
因爲在從磁盤顯示數據時從網絡加載數據是一種常見的用例,所以我們將創建一個NetworkBoundResource
可以在多個地方重用的輔助類。以下是決策樹 NetworkBoundResource
:
它首先觀察資源的數據庫。第一次從數據庫加載條目時,NetworkBoundResource
檢查結果是否足以分派和/或是否應從網絡中獲取。請注意,這兩個都可以同時發生,因爲您可能希望在從網絡更新緩存數據時顯示緩存數據。
如果網絡調用成功完成,它會將響應保存到數據庫中並重新初始化流。如果網絡請求失敗,我們會直接發送故障。
注意:將新數據保存到磁盤後,我們會重新初始化數據庫中的流,但通常我們不需要這樣做,因爲數據庫將調度更改。另一方面,依靠數據庫來調度更改將依賴於副作用,這是不好的,因爲如果數據沒有改變,數據庫可以避免調度更改,它可能會中斷。我們也不希望調度從網絡到達的結果,因爲這將違背單一事實來源(可能在數據庫中存在將更改保存值的觸發器)。我們也不想在SUCCESS
沒有新數據的情況下發送,因爲它會向客戶端發送錯誤的信息。
以下是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, implemented
// in the base class.
public final LiveData<Resource<ResultType>> getAsLiveData();
}
請注意,上面的類定義了兩個類型參數(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();
// 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();
}
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
現在,我們可以使用use 在存儲庫中NetworkBoundResource
編寫磁盤和網絡綁定 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();
}
}