本指南適用於那些過去構建應用程式有基礎知識,現在想知道構建強大的生產質量應用程式最佳實踐和建議的體系結構的開發人員。
注意:本指南假設讀者熟悉Android框架。如果您不熟悉應用程式開發,請檢視入門培訓系列,其中包含本指南的必備主題。
APP開發者面臨的常見問題
與傳統的桌面應用程式不同,Android應用程式的結構要複雜得多,在大多數情況下,它們只有一個啟動快捷方式的入口點,並且可以作為一個單一的整體程式執行。一個典型的Android應用程式是由多個應用程式元件構成的,包括活動,片段,服務,內容提供者和廣播接收器。
大多數這些應用程式元件都是在Android作業系統使用的應用程式清單中宣告的,清單決定如何將您的應用程式與其裝置的整體使用者體驗的整合。如前所述,桌面應用程式傳統上是以整體的方式執行,但正確的Android應用程式編寫需要更加靈活,因為使用者可以在裝置上的不同應用程式不斷切換流程和任務。
例如,請考慮在您最喜愛的社交網路應用程式中分享照片時會發生什麼情況。該應用程式觸發Android作業系統,啟動相機應用程式來處理請求的相機意圖。此時,使用者離開了社交網路應用,但他們的體驗是無縫的。相機應用程式又可能觸發其他意圖,例如啟動檔案選擇器,該檔案選擇器可以啟動另一個應用程式。終端使用者回到社交網路應用程式並分享照片。此外,使用者在這個過程的任何時候都可能被電話打斷,並在打完電話後回來分享照片。
在Android中,這種應用程式跳轉行為很常見,所以您的應用程式必須正確處理這些流程。請記住,移動裝置是資源受限,所以在任何時候,作業系統可能需要殺死一些應用程式,以騰出空間給新的的應用或程式。所有這一切的關鍵是,您的應用程式元件可以單獨和無序地啟動,並可以在任何時候由使用者或系統銷燬。由於應用程式元件是短暫的,它們的生命週期(建立和銷燬時)不在您的控制之下,因此您不應該在應用程式元件中儲存任何應用程式資料或狀態,並且應用程式元件不應相互依賴。
常見的架構原則
如果您不能使用應用程式元件來儲存應用程式資料和狀態,應該如何構建應用程式?
你應該關注的最重要的事情是在你的應用程式中,將所有程式碼寫入Activity或Fragment是一個常見的錯誤。任何不處理UI或作業系統互動的程式碼都不應該在這些類中。儘可能保持精簡可以避免許多生命週期相關的問題。記住,你不擁有這些類,它們只是體現作業系統和你的應用程式之間的契約的粘合劑。 Android作業系統可能會隨時根據使用者互動或其他因素(如低記憶體)來銷燬它們。最好最大限度地減少對他們的依賴,以提供可靠的使用者體驗。
第二個重要的原則是你應該從一個模型驅動(資料)你的UI,最好是一個持久模型。永續性是理想的,原因有兩個:如果作業系統破壞您的應用程式以釋放資源,則您的使用者不會丟失資料,即使網路連線不穩定或連線不上,您的應用程式也將繼續工作。模型是負責處理應用程式資料的元件。它們獨立於應用程式中的檢視和應用程式元件,因此它們與這些元件的生命週期問題是隔離的。保持簡單的UI程式碼和減少的應用程式邏輯,使管理更容易。將您的應用程式放在具有明確定義的管理資料的模型類上,使它們可測試,並能使您的應用程式保持一致。
推薦的應用架構
在本節中,我們將演示如何通過使用用例來構建使用體系結構元件的應用程式。
注意:不可能有一種編寫應用程式的方法,對每種情況都是最好的。也就是說,這個推薦的架構應該是大多數用例的一個很好的起點。如果您已經有了編寫Android應用的好方法,則不需要更改。想象一下,我們正在構建一個顯示使用者配置檔案的使用者介面。該使用者配置檔案將使用REST API從我們自己的後端獲取資料。
假如,我們正在構建一個顯示使用者資訊的使用者介面。該使用者資訊將使用REST API從我們自己的後端獲取。
構建使用者介面
使用者介面將由fragment UserProfileFragment.java 和 佈局檔案user_profile_layout.xml組成。
為了驅動使用者介面,我們的資料模型需要儲存兩個資料元素。
-
User ID:使用者的識別符號。最好使用Fragment引數將此資訊傳遞到Fragment中。如果Android作業系統破壞您的程式,這些資訊將被保留,以便在您的應用下次重新啟動時可用。
-
User物件:持有使用者資料的POJO
我們將建立一個基於ViewModel類的UserProfileViewModel來儲存這些資訊。
ViewModel為特定的UI元件(如Fragment或Activity)提供資料,並處理與資料處理業務部分的通訊,例如呼叫其他元件來載入資料或轉發使用者修改。 ViewModel不知道UI,並且不受配置更改的影響,例如由於旋轉而重新建立Activity。
現在我們有3個檔案:
-
user_profile.xml
-
UserProfileViewModel.java
-
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 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的使用者欄位被設定,我們需要一種方式來通知使用者介面。這是使用LiveData類的地方。
LiveData是一個可觀察的資料持有者。它允許應用程式中的元件觀察LiveData物件的更改,而不會在它們之間建立明確的和嚴格的依賴關係路徑。 LiveData還尊重您的應用程式元件(活動,片段,服務)的生命週期狀態,並做正確的事情來防止物件洩漏,使您的應用程式不消耗更多的記憶體。
注意:如果您已經在使用類似RxJava或Agera的庫,則可以繼續使用它們而不是LiveData。但是,當您使用它們或其他方法時,請確保正確處理生命週期,以便在相關的LifecycleOwner停止時停止資料流,並在銷燬LifecycleOwner時銷燬資料流。您還可以新增android.arch.lifecycle:reactivestreams工件以將LiveData與另一個反應流庫(例如RxJava2)一起使用。
現在我們將UserProfileViewModel中的User欄位替換為一個LiveData ,以便在資料更新時通知這個分段。 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 -> {
// update UI
});
}複製程式碼
每次更新使用者資料時,都會呼叫onChanged回撥,並重新整理UI。
如果您熟悉使用可觀察回撥的其他庫,您可能已經意識到,我們不必重寫片段的onStop()方法來停止觀察資料。這對於LiveData來說並不是必須的,因為它是生命週期感知的,這意味著它不會呼叫回撥,除非片段處於活動狀態(收到onStart(),但沒有收到onStop())。當片段收到onDestroy()時,LiveData也會自動移除觀察者。
我們也沒有做任何特殊的處理配置變化(例如,使用者旋轉螢幕)。當配置改變時,ViewModel會自動恢復,所以一旦新的片段生效,它將接收到同一個ViewModel的例項,回撥將被立即呼叫當前資料。這就是ViewModel不能直接引用Views的原因。他們可以超越View的生命週期。請參閱ViewModel的生命週期。
獲取資料
現在我們已經將ViewModel連線到了片段,但是ViewModel如何獲取使用者資料呢?在這個例子中,我們假設我們的後端提供了一個REST API。我們將使用Retrofit庫來訪問我們的後端,儘管您可以自由使用不同的庫來達到同樣的目的。
這裡是我們的改進的Web服務與我們的後端進行通訊:
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的一個原始的實現可以直接呼叫Web服務來獲取資料並將其分配給使用者物件。即使它可行,您的應用程式也將難以維持。它給ViewModel類提供了太多的責任,這違背了前面提到的分離原則。此外,ViewModel的範圍與活動或片段生命週期相關聯,因此,在生命週期結束時丟失所有資料是不好的使用者體驗。相反,我們的ViewModel將這個工作委託給一個新的Repository模組。
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;
}
}複製程式碼
即使Repository模組看起來沒有必要,它也有著重要的作用。它從應用程式的其餘部分提取資料來源。現在我們的ViewModel不知道資料是由Webservice獲取的,這意味著我們可以根據需要將其交換為其他實現。
注意:為簡單起見,我們忽略了網路錯誤的情況。有關公開錯誤和載入狀態的替代實現,請參閱附錄:公開網路狀態。
管理元件之間的依賴關係:
上面的UserRepository類需要Webservice的一個例項來完成它的工作。它可以簡單地建立它,但要做到這一點,它也需要知道Webservice類的依賴關係來構造它。這會使程式碼複雜化和重複(例如,每個需要Webservice例項的類都需要知道如何用它的依賴關係來構造它)。此外,UserRepository可能不是唯一需要Web服務的類。如果每個類建立一個新的WebService,這將是非常資源沉重。
有兩種模式可以用來解決這個問題:
-
依賴注入:依賴注入允許類在不構造它們的情況下定義它們的依賴關係。在執行時,另一個類負責提供這些依賴關係。我們推薦Google的Dagger 2庫在Android應用程式中實現依賴注入。 Dagger 2通過遍歷依賴關係樹來自動構造物件,併為依賴關係提供編譯時間保證。
-
服務定位器:服務定位器提供了一個登錄檔,類可以獲得它們的依賴而不是構建它們。實現起來比依賴注入(DI)更容易,所以如果你不熟悉DI,可以使用Service Locator。
這些模式允許您擴充套件您的程式碼,因為它們提供了用於管理依賴關係的清晰模式,無需重複程式碼或增加複雜性。他們兩人也允許交換實現測試;這是使用它們的主要好處之一
在這個例子中,我們將使用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中,它將把使用者物件快取在記憶體中。
@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永續性庫優點的地方。
Room是一個物件對映庫,提供本地資料永續性和最小的樣板程式碼。在編譯時,它會根據模式驗證每個查詢,以便斷開的SQL查詢導致編譯時錯誤,而不是執行時失敗。Room抽象出一些使用原始SQL表和查詢的底層實現細節。它還允許觀察對資料庫資料(包括集合和連線查詢)的更改,通過LiveData物件公開這些更改。另外,它明確定義瞭解決常見問題的執行緒約束,例如訪問主執行緒上的儲存。
注意:如果您的應用程式已經使用另一個永續性解決方案(如SQLite物件關係對映(ORM)),則不需要使用Room替換現有的解決方案。但是,如果您正在編寫新的應用程式或重構現有應用程式,我們建議使用Room來儲存應用程式的資料。這樣,您可以利用庫的抽象和查詢驗證功能。
要使用Room,我們需要定義我們的本地模式。首先,使用@Entity註釋User類,將其標記為資料庫中的表。
@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自動提供一個實施。有關詳細資訊,請參見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 。Room知道資料庫何時被修改,當資料改變時它會自動通知所有活動的觀察者。因為它使用的是LiveData,所以這將是有效的,因為它只會在至少有一個活動觀察者的情況下更新資料。
注意: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 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。這是抽象提供的靈活性。這對於測試也很好,因為在測試UserProfileViewModel的時候可以提供一個偽造的UserRepository。
現在我們的程式碼是完整的。如果使用者以後回到相同的使用者介面,他們會立即看到使用者資訊,因為我們持久化了。同時,如果資料陳舊,我們的倉庫將在後臺更新資料。當然,根據您的使用情況,如果資料太舊,您可能不希望顯示持久資料。
在一些使用情況下,如下拉重新整理,UI顯示使用者是否正在進行網路操作是非常重要的。將UI操作與實際資料分開是一種很好的做法,因為它可能因各種原因而更新(例如,如果我們獲取朋友列表,則可能會再次觸發同一使用者,觸發LiveData 更新)。從使用者介面的角度來看,一個正在進行中請求只是另一個資料點,類似於其他任何資料(如使用者物件)。
這個用例有兩個常見的解決方案:
-
更改getUser以返回包含網路操作狀態的LiveData。附錄中提供了一個示例實現:公開網路狀態部分。
-
在儲存庫類中提供另一個可以返回使用者重新整理狀態的公共函式。如果只想響應顯式的使用者操作(如下拉重新整理)來顯示網路狀態,則此選項更好。
單一的事實來源
不同的REST API端點通常返回相同的資料。例如,如果我們的後端擁有另一個返回朋友列表的端點,則同一個使用者物件可能來自兩個不同的API端點,也許端點不同。如果UserRepository原樣返回來自Webservice請求的響應,那麼我們的UI可能會顯示不一致的資料,因為這些請求之間的資料可能在伺服器端發生更改。這就是為什麼在UserRepository實現中,Web服務回撥只是將資料儲存到資料庫中。然後,對資料庫的更改將觸發活動LiveData物件上的回撥。
在這個模型中,資料庫充當真實資料的單一來源,應用程式的其他部分通過儲存庫訪問它。即使您使用磁碟快取,都建議您將儲存庫資料來源指定為應用程式其餘部分的單一來源。
測試
我們已經提到分離的好處之一就是可測試性。讓我們看看我們如何測試每個程式碼模組。
-
使用者介面:這將是您唯一需要Android UI Instrumentation測試的時間。測試UI程式碼的最好方法是建立一個Espresso測試。您可以建立片段併為其提供一個模擬的ViewModel。由於該片段只與ViewModel交談,因此模擬它將足以完全測試此UI。
-
ViewModel: ViewModel可以使用JUnit測試進行測試。你只需要模擬UserRepository來測試它。
-
UserRepository:您也可以使用JUnit測試來測試UserRepository。你需要模擬Web服務和DAO。您可以測試它是否進行正確的Web服務呼叫,將結果儲存到資料庫中,如果資料已快取且最新,則不會發出任何不必要的請求。既然Webservice和UserDao都是介面,你可以模擬它們或者為更復雜的測試用例建立假實現。
-
UserDao: 測試DAO類的推薦方法是使用儀器測試。由於這些測試不需要任何使用者介面,他們仍然會執行得很快。對於每個測試,您可以建立一個記憶體資料庫,以確保測試沒有任何副作用(如更改磁碟上的資料庫檔案)。Room還允許指定資料庫實現,以便通過提供SupportSQLiteOpenHelper的JUnit實現來測試它。通常不建議使用這種方法,因為裝置上執行的SQLite版本可能與主機上的SQLite版本不同。
-
Webservice:使測試獨立於外界是很重要的,所以即使是Web服務測試也應該避免對後端進行網路呼叫。有很多library可以幫助你。例如,MockWebServer是一個偉大的庫,可以幫助您為測試建立一個假的本地伺服器。
-
Testing Artifacts: 架構元件提供了一個maven工件來控制其後臺執行緒。在android.arch.core:核心測試工件內部,有2個JUnit規則:
- InstantTaskExecutorRule:此規則可用於強制架構元件立即在呼叫執行緒上執行任何後臺操作。
- CountingTaskExecutorRule:此規則可用於檢測測試,以等待體系結構元件的後臺操作或將其作為閒置資源連線到Espresso。
架構圖
下圖顯示了我們推薦的體系結構中的所有模組以及它們如何相互互動:
指導原則
程式設計是一個創造性的領域,構建Android應用程式也不是一個例外。解決問題的方法有很多種,可以在多個活動或片段之間傳遞資料,檢索遠端資料並將其儲存在本地以進行離線模式,也可以使用許多其他常見應用程式遇到的情況。
儘管以下建議不是強制性的,但是我們的經驗是,遵循這些建議將使您的程式碼基礎更加健壯,可測試和可維護。
-
您在清單中定義的入口點(活動,服務,廣播接收器等)不是資料的來源。相反,他們只應該協調與該入口點相關的資料子集。由於每個應用程式元件的壽命相當短,這取決於使用者與裝置的互動以及執行時的整體當前執行狀況,因此您不希望這些入口點中的任何一個成為資料來源。
-
在應用程式的各個模組之間建立明確界定的責任。例如,不要將從網路載入資料的程式碼跨程式碼庫中的多個類或包傳播。同樣,不要把不相關的職責 – 比如資料快取和資料繫結 – 放到同一個類中。
-
儘可能少地從每個模組公開。不要試圖建立“only one”的快捷方式,從一個模組公開內部實現細節。您可能在短期內獲得一些時間,但隨著您的程式碼庫的發展,您將多次支付技術債務。
-
在定義模組之間的互動時,請考慮如何使每個模組獨立地進行測試。例如,如果有一個定義良好的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;
}
}複製程式碼
現在,我們可以使用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();
}
}複製程式碼