Android架構組件-App架構指南,你還不收藏嘛

本指南適用于那些已經擁有開發Android應用基礎知識的開發人員,現在想了解能夠開發出更加健壯、優質的應用程序架構。

先給大家來點小福利吧,架構全套學習資料以及高清視頻教程,教你如何一步一步走上架構師之路

領取方式,關注小編私信【學】即可獲取



首先需要說明的是:Android Architecture Components 翻譯為 Android架構組件 并不是我自己隨意翻譯的,而是Google官方博客中明確稱其為 Android架構組件,因此我遵循了這種叫法。

下面這張圖是Android架構組件完整的架構圖,其中表示了的架構組件的所有模塊以及它們之間如何交互:

APP開發者面臨的常見問題

與傳統的桌面應用程序不同,Android應用程序的結構要復雜得多,在大多數情況下,它們只在桌面快捷啟動方式中有一個入口,并且作為單個進程運行。一個典型的Android應用程序是由多個 APP組件(Android四大組件) 構成的,包括?activities,?fragments,?services,?content providers?and?broadcast receivers

這些?app組件?中的大部分都是在?應用清單(AndroidManifast.xml)中聲明的,Android操作系統使用這些組件將應用程序集成到設備的用戶界面中。雖然,應用程序通常上是以單個進程運行的,但是一個合理的Android應用需要更加靈活,因為用戶可以通過不同的應用程序,在他們的設備上不斷切換流程和任務。

想象下在我們最喜愛的社交網絡應用中分享照片時會發生什么情況。首先這個應用程序觸發一個Camera(拍照或攝像)?Intent,由Android操作系統啟動一個Camera應用來處理請求。此時,用戶雖然離開了這個社交網絡應用,但他們的體驗是無縫的。相機應用程序又可能觸發其他?Intent,例如啟動文件管理器,該文件管理器可以啟動另一個應用程序,最終用戶回到社交網絡應用并分享照片。此外,用戶在這個過程的任何時候都可能被電話打斷,并在打完電話后回來繼續分享照片。

在Android中,這種應用程序跳轉行為是很常見的,所以我們的應用程序必須正確處理這些流程。請記住,移動設備是資源受限的,所以在任何時候,操作系統都可能需要殺死一些應用程序,以騰出空間給新的應用。

這一切的要點在于,我們的?app組件?可以單獨和無序地啟動,并且可以在任何時候由用戶或系統銷毀。由于?app組件?是短暫的,并且它們的生命周期(創建和銷毀時)不在我們的控制之下,因此我們不應該在app組件中存儲任何 app數據或狀態,并且 app組件不應相互依賴。

通用架構原則

如果不使用?app組件存儲app數據和狀態,那該如何構造應用程序呢?

我們需要關注的最重要的事情是:如何在你的應用中分離關注點。最常見的錯誤是將所有的代碼寫入一個 Activity 或 Fragment,任何不處理 UI 或 與操作系統交互的代碼都不應該出現在這些類中,我們應該盡可能保持 Activity 或Fragment 精簡,這樣可以避免許多生命周期相關的問題。

請記住,我們不擁有這些類,它們只是建立操作系統和我們的應用程序之間契約的膠水類。Android操作系統可能會隨時根據用戶交互或其他因素(如低內存)來銷毀它們,最好盡可能地減少依賴他們,以提供可靠的用戶體驗。

第二個重要原則是:?你應該從一個 Model 驅動你的UI,最好是一個持久化的 Model。之所以說持久化是理想的 Model,原因有兩個:如果操作系統銷毀你的應用程序以釋放資源,那么你的用戶就不會丟失數據,即使網絡連接不穩定或連接不上,你的應用程序也會繼續工作。

Model 是負責處理應用程序數據的組件,它們獨立于應用程序的 Views 和 app組件,因此 Model 與這些 app組件的生命周期問題是相隔離的。保持簡潔的UI代碼,以及不受約束的應用程序邏輯,可以使app的管理更加容易,基于具有明確定義的管理數據責任的模型類的應用程序,會更加具有可測試性,并使我們的應用程序狀態保持前后一致。

推薦的App架構

在本節中,我們將演示如何通過使用用例來構造使用了?架構組件(Architecture Components)?的應用程序。

注意:不可能有一種編寫應用程序的方法對每個場景都是最好的。對于大多數用例來說,推薦的這個架構可能是一個好的起點。如果你已經有了編寫Android應用的好方法,那就不要在更改了。

假如我沒正在搭建一個用來顯示?用戶概況的UI,該用戶概況將使用?REST API?從我們自己的服務器端獲取。

搭建用戶界面

這個UI 將由 UserProfileFragment.java 及 Fragment 相應的 user_profile_layout.xml 布局文件組成。

為了驅動用戶界面,我們的數據模型需要保存兩個數據元素。

用戶ID:用戶的標識符。最好使用 fragment 參數(setArguments方法) 將此信息傳遞到 fragment 中。如果Android系統銷毀了你的進程,這些信息將被保留,便于應用在下次重新啟動時可用。

用戶對象:保存用戶數據的?POJO(簡單的Java對象)

我們將創建一個基于ViewModel?的 UserProfileViewModel 類來保存這些信息。

一個?ViewModel?提供了一個特定 UI 組件中的數據,如一個 fragment 或 activity, 并且負責與數據處理業務的通信,例如調用其他 app組件 來加載數據或轉發用戶信息的修改。ViewModel不知道View,并且不受配置更改的影響,例如由于屏幕旋轉而重新創建 Activity。

現在我們有3個文件。

user_profile.xml:定義屏幕上的 UI。

UserProfileViewModel.java:為 UI 準備數據的類。

UserProfileFragment.java:顯示?ViewModel?中的數據并對用戶交互作出響應的 UI 控制器。

接下來我們將開始實現(為了簡單起見,省略了布局文件):

publicclassUserProfileViewModelextendsViewModel{

privateString userId;

privateUser user;

publicvoidinit(String userId){

this.userId = userId;

}

publicUsergetUser(){

returnuser;

}

}12345678910111213

publicclassUserProfileFragmentextendsFragment{

privatestaticfinalString UID_KEY ="uid";

privateUserProfileViewModel viewModel;

@Override

publicvoidonActivityCreated(@Nullable Bundle savedInstanceState){

super.onActivityCreated(savedInstanceState);

String userId = getArguments().getString(UID_KEY);

viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);

viewModel.init(userId);

}

@Override

publicViewonCreateView(LayoutInflater inflater,

? ? ? ? ? ? ? ? @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){

returninflater.inflate(R.layout.user_profile, container,false);

}

}123456789101112131415161718

現在,我們如何將它們聯系在一起呢?當給?ViewModel?的 User 字段設值后,我們需要一種方法來通知用戶界面,這就是?LiveData類的作用。

LiveData?是一個可觀察的數據持有者。它允許應用程序中的組件觀察 LiveData 對象的更改,但不會在它們之間創建明確的和嚴格的依賴關系路徑。?LiveData?還會關聯?app組件(activities, fragments, services)?的生命周期狀態,并做出合適的事情來防止內存泄漏。

注意:如果你已經在使用類似?RxJava?或?Agera?的庫 ,則可以繼續使用它們而不是LiveData。但是,當你使用它們或其他方式時,請確保正確處理生命周期,以便在相關的LifecycleOwner 停止時暫停數據流,并在銷毀 LifecycleOwner 時銷毀數據流。你還可以添加android.arch.lifecycle:reactivestreams?以將?LiveData?與其他的響應流庫(例如RxJava2)一起使用。

現在我們用 LiveData 替換 UserProfileViewModel 中的 User 字段,以便在數據更新時通知 Fragment。最主要的是:LiveData是生命周期感知的,并且在不在需要時,它將自動清理引用。

publicclassUserProfileViewModelextendsViewModel{

...

privateUser user;

privateLiveData user;

publicLiveDatagetUser(){

returnuser;

}

}123456789

現在我們修改 UserProfileFragment 以便觀察數據并更新 UI。

@Override

publicvoid onActivityCreated(@NullableBundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);

viewModel.getUser().observe(this, user -> {

// update UI

? ? });

}1234567

每次更新用戶數據時, 都會調用 onChanged 回調,并刷新 UI。

如果你熟悉其他 可觀察回調的庫,你可能已經意識到,我們沒有重寫 fragment 的 onStop() 方法來停止觀察數據。這對于?LiveData來說是沒有必要的,因為它是生命周期感知的,這意味著它不會調用回調,除非 Fragment 處于?活動狀態(已收到 onStart() 但未收到 onStop())。當 fragment 收到 onDestroy() 時,LiveData也將自動移除觀察者 。

對于配置變化(例如,用戶旋轉屏幕)我們也沒有做任何特殊的處理。當配置改變時,ViewModel?會自動恢復,所以一旦新的 Fragment 生效,它將接收到相同的?ViewModel實例,并且 ViewModel 的回調將立即被當前數據調用,這就是 ViewModels 為什么不應該直接引用 Views 的原因,他們可以比 View的生命周期更持久。想了解更多信息的請查看 The lifecycle of a ViewModel 。

獲取數據

現在我們已經將?ViewModel?關聯到了 Fragment,但是?ViewModel?如何獲取用戶數據呢?在這個例子中,我們假設服務器端提供了一個?REST API。我們將使用 Retrofit 庫來訪問我們的服務器端,雖然你可以自由使用不同的庫來達到同樣的目的。

下面是retrofit 的?Webservice?,負責與服務器端進行通信:

publicinterfaceWebservice{

/**

? ? * @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 getUser(@Path("user") String userId);

}123456789

ViewModel 的一個簡單實現是直接調用 Webservice 來獲取數據并將其 賦值給 user 對象,雖然這樣是可行的,但是我們的應用程序以后將很難維護。它賦予了 ViewModel 類太多的職責,違背了我們前面提到的關注點分離原則。此外,ViewModel 的作用域與一個 Activity 或一個 Fragment 生命周期相關聯,當他們的生命周期完成時將丟失所有的數據,這是非常糟糕的用戶體驗。因此,我們將 ViewModel 的這個工作委托給了一個新的模塊?Repository?。

Repository?模塊負責數據處理操作。他們為應用的其余部分提供了一個干凈的API,他們知道從何處獲取數據以及在更新數據時調用哪些API。你可以將它們視為不同數據源 (持久化模型, web服務, 緩存, etc.)之間的中介。

UserRepository 類使用 WebService 來獲取用戶數據項,如下:

publicclassUserRepository{

privateWebservice 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;

? ? }

}1234567891011121314151617

雖然?repository?模塊看起來沒有必要,但是它有一個重要的目的,它從應用程序的其余部分提取數據源。現在我們的 ViewModel 不知道數據是從 Webservice 獲取到的,這意味著我們可以根據需要,將它(Webservice)替換為其他的實現。

注意:為了簡單起見,我們忽略了網絡錯誤的情況。對于暴露錯誤和加載狀態的另一個實現,請查看?附錄:暴露網絡狀態

管理組件之間的依賴關系:

上面的 UserRepository 類需要一個 Webservice 的實例來工作,UserRepository 可以簡單地創建Webservice ,但要做到這一點,它必須需要知道 Webservice 類的依賴關系來構造它,這會使代碼顯著和成倍的復雜化(例如,每個需要 Webservice實例的類 都需要知道如何用它的依賴來構造它)。另外,UserRepository 可能不是唯一需要 Webservice 的類。如果每個類創建一個新的 WebService,這將是非常沉重的資源。

現在我們有兩種模式可以用來解決這個問題:

依賴注入:依賴注入允許類在不構造它們的情況下定義它們的依賴關系。在運行時,另一個類負責提供這些依賴關系。我們推薦 Google 的 Dagger 2 庫,在Android應用中實現依賴注入。Dagger 2 通過遍歷依賴關系樹來自動構造對象,并為依賴關系提供編譯時間保證。

服務定位器:服務定位器提供了一個注冊表,這個類可以獲得它們的依賴 而不是 構建它們。實現起來比依賴注入(DI)更容易,所以如果你不熟悉DI,可以使用 Service Locator。

這些模式允許您擴展代碼,因為它們提供了用于管理依賴關系的清晰模式,無需復制代碼或增加復雜性。這兩個模式也允許交換實現測試, 這是使用它們的主要好處之一。

在這個例子中,我們將使用 依賴注入 來管理依賴關系。

關聯ViewModel和repository

現在我們修改 UserProfileViewModel 使用的 repository。

publicclassUserProfileViewModelextendsViewModel{

privateLiveData user;

privateUserRepository 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;

? ? }

}1234567891011121314151617181920212223

緩存數據

上面的 repository 實現 對抽象調用 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 LiveDatagetUser(String userId) {

LiveDatacached = userCache.get(userId);

if (cached != null) {

return cached;

}

final MutableLiveDatadata = 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() {

@Override

public void onResponse(Callcall, Responseresponse) {

data.setValue(response.body());

}

});

return data;

}

}123456789101112131415161718192021222324

持久化數據

在我們當前的實現中,如果用戶旋轉屏幕或離開并返回到應用,現有UI將立即可見,因為 repository 從內存中檢索緩存的數據。但是,如果用戶離開應用程序并且數小時后回來,或Android 系統殺死該進程后,會發生什么?

在目前的實現中,我們將需要從網絡上重新獲取數據。這不僅是一個糟糕的用戶體驗,而且會浪費資源,因為它會使用移動數據重新獲取相同的數據。你可以簡單地通過緩存Web請求來解決這個問題,但是這會產生新的問題。如果相同的用戶數據從另一種類型的請求中顯示出來(例如,獲取朋友列表),會發生什么情況?那么你的應用程序可能會顯示不一致的數據,這是一個混亂的用戶體驗。例如,由于好友列表請求和用戶請求可以在不同的時間執行,所以相同用戶的數據可能會以不同的方式顯示。您的應用需要合并它們以避免顯示不一致的數據。

處理這個問題的正確方法是使用?持久化模型。這就是 Room 持久化庫可以拯救的地方。

Room 是一個對象映射庫,使用最小的模板代碼來提供本地數據持久化。在編譯時,它會根據?Schema?驗證每個查詢,因此,有問題的SQL查詢會導致編譯時出錯,而不是運行時失敗。Room 抽象了處理原始SQL表和查詢的一些底層實現細節。它還允許觀察對數據庫數據(包括集合和 join 查詢)的更改,通過?LiveData對象 公開這些更改 。另外,它明確定義了解決常見問題的線程約束,例如在主線程上的訪問存儲。

注意:如果你的應用程序已經使用另一個持久化解決方案(如SQLite對象關系映射(ORM)),則不需要使用 Room 替換現有的解決方案。但是,如果你正在編寫新的應用程序或重構現有的應用程序,我們建議使用 Room 來保存應用程序的數據。這樣,你可以利用庫的抽象和查詢 驗證功能。

要使用 Room,我們需要定義我們的本地?Schema。首先,使用?@Entity?注解 User 類 以將其標記為數據庫中的表。

@Entity

classUser {

@PrimaryKey

privateint id;

privateStringname;

privateStringlastName;

// getters and setters for fields

}12345678

然后,為我們的 app 創建一個數據庫類繼承于?RoomDatabase

@Database(entities = {User.class},version=1)

publicabstractclassMyDatabaseextendsRoomDatabase{

}123

注意?MyDatabase?是抽象的。Room 自動提供一個它的實現。有關詳細信息,請查看 Android架構組件- Room數據庫的使用

現在我們需要一種將用戶數據插入數據庫的方法。為此,我們將創建一個數據訪問對象(DAO: data access object)

@Dao

publicinterfaceUserDao{

@Insert(onConflict = REPLACE)

voidsave(User user);

@Query("SELECT * FROM user WHERE id = :userId")

LiveDataload(String userId);

}1234567

然后,從我們的數據庫類中引用?DAO (Data Access Object)

@Database(entities = {User.class},version=1)

publicabstractclassMyDatabaseextendsRoomDatabase{

publicabstractUserDaouserDao();

}1234

請注意,該?load?方法返回一個?LiveData。Room 知道數據庫何時被修改,當數據改變時它會自動通知所有活躍的的察者。因為它使用的是 LiveData,所以這將是有效的,因為只有至少有一個活動的觀察者才會更新數據。

注意:Room 根據 table 的修改來檢查失效,這意味著它可能發送誤報的通知。

現在我們可以修改?UserRepository?來包含 Room 數據源。

@Singleton

publicclassUserRepository{

privatefinalWebservice webservice;

privatefinalUserDao userDao;

privatefinalExecutor executor;

@Inject

publicUserRepository(Webservice webservice, UserDao userDao, Executor executor) {

this.webservice = webservice;

this.userDao = userDao;

this.executor = executor;

}

publicLiveData 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());

? ? ? ? ? ? }

? ? ? ? });

? ? }

}1234567891011121314151617181920212223242526272829303132333435

請注意,盡管我們改變了 來自于 UserRepository 的數據,我們并不需要改變我們 UserProfileViewModel 或 UserProfileFragment。這是抽象提供的靈活性。這對于測試來說有好處的,因為你可以在測試你的UserProfileViewModel 的時候提供一個假的 UserRepository。

現在我們的代碼是完整了。如果用戶以后回到相同的用戶界面,他們會立即看到用戶信息,因為我們持久化了。同時,如果數據過期了,我們的倉庫將在后臺更新數據。當然,根據您的使用情況,如果數據太舊,您可能不希望顯示持久化數據。

在一些使用情況下,如?下拉刷新,UI 顯示用戶是否正在進行網絡操作是非常重要的。將UI 操作與實際數據分開是一個很好的做法,因為它可能因各種原因而導致更新(例如,如果我們獲取朋友列表,同一用戶可能會再次觸發 LiveData 更新)。站在UI 的角度,事實上,當有一個請求執行的時候,另一個數據點,類似于任何其他的數據 (比如 User 對象)。

這個用例有兩種常見的解決方案:

更改 getUser 為返回包含網絡操作狀態的 LiveData 。附錄中提供了一個示例實現:公開網絡狀態部分。

在 repository 類中提供另一個可以返回用戶刷新狀態的公共函數。如果只想響應顯式的用戶操作(如下拉刷新)來顯示網絡狀態,則此選項更好。

單一的真相來源:

不同的?REST API?端點通常返回相同的數據。例如,如果我們的服務器端擁有另一個返回 朋友列表的端點,則同一個用戶對象可能來自兩個不同的API 端點,也許粒度不同。如果 UserRepository 從 Webservice請求返回原本的響應,我們的UI可能會顯示不一致的數據,因為在這些請求過程中數據可能已經在服務器端發生了改變。這就是為什么在 UserRepository 實現中,Web服務回調只是將數據保存到數據庫中。然后,對數據庫的更改將觸發回調給 活躍狀態的 LiveData 對象。

在這個模型中,數據庫充當了?單一的真相來源,應用程序的其他部分通過 Repository 訪問它。無論你是否使用磁盤緩存,我們都建議將你的 Repository 指定為應用程序其余部分唯一的真相來源。

測試

我們已經提到分離的好處之一就是可測試性,讓我們看看如何測試每個代碼模塊。

用戶界面和交互:你唯一需要花費時間的是 Android UI Instrumentation 。測試UI 代碼的最好方法是創建一個 Espresso測試。您可以創建 Fragment 并為其提供一個模擬的ViewModel。由于該 Fragment 只與 ViewModel 聯系,所以偽造它足以完全測試這個UI。

ViewModel:ViewModel 可以使用 JUnit 來測試 。你只需要模擬 UserRepository 來測試它。

UserRepository:你同樣也可以使用 JUnit 來測試 UserRepository。你需要模擬 Webservice 和 DAO。你可以測試它是否做出了正確的Web服務調用,并將結果保存到數據庫中,如果數據已緩存且最新,則不會發出任何不必要的請求。因為 Webservice 和 UserDao 都是接口,你可以模擬它們,或者為更復雜的測試用例創建偽造的實現…

UserDao:測試 DAO 類的推薦方法是使用 instrumentation 測試。由于這些 instrumentation 測試不需要任何用戶界面,他們將會運行得很快。對于每個測試,您可以創建一個處于內存中的數據庫,以確保測試沒有任何副作用(如更改磁盤上的數據庫文件)。 Room 也允許指定數據庫的實現,所以你可以通過提供 JUnit 來測試 SupportSQLiteOpenHelper 的實現。通常不建議使用這種方法,因為設備上運行的SQLite版本可能與主機上的SQLite版本不同。

Webservice:使測試獨立于外界是很重要的,所以你的 Webservice 測試也應該避免對后端進行網絡調用。有很多庫可以幫助你,例如, MockWebServer 是一個強大的庫,可以幫助你為測試創建一個偽造的本地服務器。

Testing Artifacts?架構組件提供了一個Maven artifact 來控制其后臺線程。在android.arch.core:core-testing artifact 內部 ,有2個 JUnit 規則:InstantTaskExecutorRule:此規則可用于強制架構組件立即在調用線程上執行任何后臺操作。CountingTaskExecutorRule:此規則可用于檢測測試,以等待架構組件的后臺操作或將其作為閑置資源連接到 Espresso。

最終的架構

下圖顯示了我們推薦的架構中的所有模塊以及它們如何相互交互:

指導原則

編程是一個創造性的領域,開發Android應用也不例外。解決問題的方法有很多種,可以在多個Activity 或 Fragment 之間傳遞數據,檢索遠程數據并將其保存在本地以進行離線模式,也可以使用其他常見應用程序遇到的情況。

雖然以下建議不是強制性的,但是根據我們的經驗,從長遠來看遵循這些建議將使您的代碼更加健壯,變得可測試和可維護。

你在 AndroidManifest 中定義的入口點(activities, services, broadcast receivers, 等等)不是數據的來源。相反,他們只應該協調與該入口點相關的數據子集。由于每個 app組件的 存活相當短,這取決于用戶與設備的交互以及當前運行時的狀況,因此你不希望這些入口點中的任何一個成為數據源。

在應用程序的各個模塊之間建立明確的職責界限時要毫不留情。例如,不要將從網絡中加載數據的代碼分散到多個類或包中。同樣,不要把不相關的職責 - 比如數據緩存和數據綁定 - 放到同一個類中。

盡可能少地暴露每個模塊。不要試圖創建“只有那一個”的快捷方式,從一個模塊公開其內部實現細節。你可能會在短期內獲得一些時間,但隨著代碼庫的不斷發展,你將會花費更多時間付出技術代價。

在定義模塊之間的交互時,請考慮如何使每個模塊獨立地進行測試。例如,如果有一個定義良好的 API 從網絡中獲取數據,測試將數據保存在本地數據庫中的模塊會變得更容易。相反,如果將這兩個模塊的邏輯混合在一起,或者在整個代碼庫中分散網絡請求代碼,那么要測試就會更加困難。

你的APP的核心是讓它從其他APP中脫穎而出。不要花費時間重新造輪子,或者一次又一次地寫出相同的樣板代碼。相反,將精力集中在可以讓你的應用獨特的東西上,讓Android 架構 和其他推薦的庫處理重復的樣板代碼。

持久化盡可能多的相關和最新的數據,以便當設備處于離線模式時你的APP依然可用。雖然你可能喜歡恒定的高速連接,但你的用戶可能并不會。

你的 repository 應該指定一個數據源作為單一的事實來源。無論你的應用程序何時需要訪問這些數據,都應始終從單一的事實源頭發起。有關更多信息,請查看?單一的真相來源

附錄:暴露網絡狀態

在上面推薦的App架構部分,我們故意省略了網絡錯誤和加載狀態,以保持示例的簡單。在本節中,我們演示了如何使用 Resource 類來暴露網絡狀態以及封裝數據及其狀態。

以下是一個實現的例子:

//a generic class that describes a data with a status

public class Resource {

@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 Resource success(@NonNull T data) {

returnnew Resource<>(SUCCESS, data, null);

}

public static Resource error(String msg, @Nullable T data) {

returnnew Resource<>(ERROR, data, msg);

}

public static Resource loading(@Nullable T data) {

returnnew Resource<>(LOADING, data, null);

}

}1234567891011121314151617181920212223

因為從網絡加載數據,而從磁盤顯示數據是一個常見的用例,我們將創建一個輔助類 NetworkBoundResource ,它可以在多個地方重復使用。以下是 NetworkBoundResource 的決策樹:

它首先通過對資源的數據庫進行觀察。當第一次從數據庫加載條目時,**NetworkBoundResource**0 會檢查結果是否足夠好以便被分發,或者它應該從網絡中獲取。請注意,這兩種情況可能同時發生,因為你可能希望在從網絡獲取數據時顯示緩存的數據。

如果網絡調用成功完成,則將響應保存到數據庫中并重新初始化流。如果網絡請求失敗,我們直接發送失敗。

注意:在將新數據保存到磁盤之后,我們會重新初始化數據庫中的數據流,但通常我們不需要這樣做,因為數據庫將會發送更改。另一方面,依靠數據庫來發送更改,將產生依賴副作用,因為如果數據沒有變化,數據庫可以避免發送變化,那么它可能會中斷。我們也不希望發送從網絡返回的結果,因為這將違背單一的真相來源(也許數據庫中會有觸發機制可以改變保存的值)。我們也不想在沒有新數據的情況下發送 SUCCESS,因為它會向客戶發送錯誤的信息。

下面是 公開API 是為?NetworkBoundResource?的子類提供的 :

// 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();

}123456789101112131415161718192021222324252627282930

請注意,上面的類定義了兩個類型參數(ResultType, RequestType),因為從 API 返回的數據類型可能與本地使用的數據類型不匹配。

另請注意,上面的 ApiResponse 代碼用于網絡請求。 ApiResponse是一個簡單的Retrofit2.Call類包裝,將其響應轉換為 LiveData。

以下是該 NetworkBoundResource 類的其余部分:

publicabstractclassNetworkBoundResource{

privatefinalMediatorLiveData> result =newMediatorLiveData<>();

@MainThread

NetworkBoundResource() {

result.setValue(Resource.loading(null));

LiveData dbSource = loadFromDb();

result.addSource(dbSource, data -> {

result.removeSource(dbSource);

if(shouldFetch(data)) {

fetchFromNetwork(dbSource);

}else{

result.addSource(dbSource,

newData -> result.setValue(Resource.success(newData)));

}

});

}

privatevoidfetchFromNetwork(finalLiveData dbSource){

LiveData> 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;

? ? }

}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

現在,我們可以使用 NetworkBoundResource 將在 repository 中綁定 User 實現 寫入我們的磁盤和網絡。

classUserRepository{

Webservice webservice;

UserDao userDao;

publicLiveData> loadUser(finalString userId) {

returnnew NetworkBoundResource() {

@Override

protectedvoid saveCallResult(@NonNullUser item) {

userDao.insert(item);

}

@Override

protectedboolean shouldFetch(@NullableUserdata) {

returnrateLimiter.canFetch(userId) && (data==null|| !isFresh(data));

}

@NonNull@Override

protectedLiveData loadFromDb() {

returnuserDao.load(userId);

}

@NonNull@Override

protectedLiveData> createCall() {

returnwebservice.getUser(userId);

}

}.getAsLiveData();

}

}

喜歡的給小編點個小關注啦,感謝!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380