本文翻譯開發者官網
App體系結構指南
本指南適用于那些過去構建應用程序的基礎知識的開發人員,現在想知道構建強大的生產質量應用程序的最佳實踐和建議的體系結構。
注意:本指南假定讀者熟悉Android框架。如果您不熟悉應用程序開發,請查看入門培訓系列,其中包含本指南的必備主題。
應用開發者面臨的常見問題
與傳統的桌面應用程序不同,Android應用程序的結構要復雜得多,在大多數情況下,它們只有一個啟動快捷方式的入口點,并且可以作為一個整體進程運行。一個典型的Android應用程序是由多個應用程序組件構成的,包括活動,片段,服務,內容提供者和廣播接收器。
大多數這些應用程序組件都是在Android操作系統使用的應用程序清單中聲明的,以決定如何將您的應用程序與其設備的整體用戶體驗相集成。雖然如前所述,桌面應用程序傳統上是以整體的方式運行的,但正確編寫的Android應用程序需要更加靈活,因為用戶可以通過設備上的不同應用程序進行編程,不斷切換流程和任務。
例如,請考慮在您最喜愛的社交網絡應用程序中分享照片時會發生什么情況。該應用程序觸發Android操作系統啟動相機應用程序來處理請求的相機意圖。此時,用戶離開了社交網絡應用,但他們的體驗是無縫的。相機應用程序又可能觸發其他意圖,例如啟動文件選擇器,該文件選擇器可以啟動另一個應用程序。最終用戶回到社交網絡應用程序并分享照片。此外,用戶在這個過程的任何時候都可能被電話打斷,并在打完電話后回來分享照片。
在Android中,這種應用程序跳轉行為很常見,所以您的應用程序必須正確處理這些流程。請記住,移動設備是資源受限,所以在任何時候,操作系統可能需要殺死一些應用程序,以騰出空間給新的。
所有這一切的關鍵是,您的應用程序組件可以單獨和無序地啟動,并可以在任何時候由用戶或系統銷毀。由于應用程序組件是短暫的,它們的生命周期(創建和銷毀時)不在您的控制之下,因此您不應該在應用程序組件中存儲任何應用程序數據或狀態,并且應用程序組件不應相互依賴。
共同的建筑原則
如果您不能使用應用程序組件來存儲應用程序數據和狀態,應該如何構建應用程序?
你應該關注的最重要的事情是在你的應用程序中分離關注點。將所有的代碼寫入一個Activity或一個常見的錯誤Fragment。任何不處理UI或操作系統交互的代碼都不應該在這些類中。盡可能保持精簡可以避免許多生命周期相關的問題。不要忘記,你不擁有這些類,它們只是體現操作系統和你的應用程序之間的契約的膠水類。Android操作系統可能會隨時根據用戶交互或其他因素(如低內存)來銷毀它們。最好最大限度地減少對他們的依賴,以提供可靠的用戶體驗。
第二個重要的原則是你應該從一個模型驅動你的UI,最好是一個持久模型。持久性是理想的,原因有兩個:如果操作系統破壞您的應用程序以釋放資源,則您的用戶不會丟失數據,即使網絡連接不穩定或連接不上,您的應用程序也將繼續工作。模型是負責處理應用程序數據的組件。它們獨立于應用程序中的視圖和應用程序組件,因此它們與這些組件的生命周期問題是隔離的。保持簡單的UI代碼和免費的應用程序邏輯,使管理更容易。將您的應用程序放在具有明確定義的管理數據責任的模型類上,將使它們可測試,并使您的應用程序保持一致。
推薦的應用架構
在本節中,我們將演示如何通過使用用例來構建使用體系結構組件的應用程序。
注意:不可能有一種編寫應用程序的方法,這對每種情況都是最好的。這就是說,這個推薦的架構應該是大多數用例的一個很好的起點。如果您已經有了編寫Android應用的好方法,則不需要更改。
想象一下,我們正在構建一個顯示用戶配置文件的用戶界面。該用戶配置文件將使用REST API從我們自己的私人后端獲取。
構建用戶界面
UI將由一個片段UserProfileFragment.java及其相應的布局文件組成user_profile_layout.xml。
為了驅動用戶界面,我們的數據模型需要保存兩個數據元素。
用戶ID:用戶的標識符。最好使用片段參數將此信息傳遞到片段中。如果Android操作系統破壞您的進程,這些信息將被保留,以便在您的應用下次重新啟動時可用。
用戶對象:保存用戶數據的POJO。
我們將創建一個UserProfileViewModel基于ViewModel的類來保存這些信息。
甲視圖模型提供了一個特定的UI組件中的數據,如一個片段或活性,和處理與數據處理的部分業務,如主叫其他組件加載數據或轉發的用戶修改的通信。ViewModel不知道視圖,并且不受配置更改的影響,例如由于旋轉而重新創建活動。
現在我們有3個文件。
user_profile.xml:屏幕的UI定義。
UserProfileViewModel.java:為UI準備數據的類。
UserProfileFragment.java:在ViewModel中顯示數據并對用戶交互作出反應的UI控制器。
下面是我們的開始的實現(布局文件為簡單起見被省略):
publicclassUserProfileViewModelextendsViewModel{privateStringuserId;privateUseruser;
publicvoidinit(StringuserId){this.userId=userId;}publicUsergetUser(){returnuser;}}
publicclassUserProfileFragmentextendsFragment{privatestaticfinalStringUID_KEY="uid";privateUserProfileViewModelviewModel;
@OverridepublicvoidonActivityCreated(@NullableBundlesavedInstanceState){super.onActivityCreated(savedInstanceState);StringuserId=getArguments().getString(UID_KEY);
viewModel
=ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel
.init(userId);}
@OverridepublicViewonCreateView(LayoutInflaterinflater,@NullableViewGroupcontainer,@NullableBundlesavedInstanceState){returninflater.inflate(R.layout.user_profile,container,false);}}
現在,我們有這三個代碼模塊,我們如何連接它們?畢竟,當ViewModel的用戶字段被設置,我們需要一種方式來通知用戶界面。這是LiveData類的地方。
LiveData是一個可觀察的數據持有者。它允許應用程序中的組件觀察LiveData對象的更改,而不會在它們之間創建明確的和嚴格的依賴關系路徑。LiveData還尊重您的應用程序組件(活動,片段,服務)的生命周期狀態,并做正確的事情來防止對象泄漏,使您的應用程序不消耗更多的內存。
注意:如果您已經在使用類似RxJava或Agera的庫,則可以繼續使用它們而不是LiveData。但是,當您使用它們或其他方法時,請確保正確處理生命周期,以便在相關的LifecycleOwner停止時停止數據流,并在銷毀LifecycleOwner時銷毀數據流。您還可以添加android.arch.lifecycle:reactivestreams工件以將LiveData與另一個反應流庫(例如RxJava2)一起使用。
現在我們用a替換User字段,以便在數據更新時通知片段。重要的是,它是生命周期感知,并將自動清理引用時,不再需要。UserProfileViewModelLiveDataLiveData
公共類UserProfileViewModel擴展ViewModel{...私人用戶的用戶;私人LiveData<用戶>用戶;publicLiveDatagetUser(){returnuser;}}
現在我們修改UserProfileFragment觀察數據并更新UI。
@OverridepublicvoidonActivityCreated(@NullableBundlesavedInstanceState){super.onActivityCreated(savedInstanceState);
viewModel
.getUser().observe(this,user->{// update UI});}
每次更新用戶數據時,都會調用onChanged回調,并刷新UI。
如果您熟悉使用可觀察回調的其他庫,您可能已經意識到,我們不必重寫片段的onStop()方法來停止觀察數據。這對于LiveData來說是不必要的,因為它是生命周期感知的,這意味著它不會調用回調,除非片段處于活動狀態(已接收onStart()但未接收onStop())。當數據片段收到時,LiveData也會自動移除觀察者onDestroy()。
我們也沒有做任何特殊的處理配置變化(例如,用戶旋轉屏幕)。當配置改變時,ViewModel會自動恢復,所以一旦新的片段生效,它將接收到相同的ViewModel實例,并且回調將被當前數據立即調用。這就是ViewModel不能直接引用Views的原因。他們可以超越View的生命周期。請參閱ViewModel的生命周期。
正在提取數據
現在我們已經將ViewModel連接到了片段,但是ViewModel如何獲取用戶數據呢?在這個例子中,我們假設我們的后端提供了一個REST API。我們將使用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}")CallgetUser(@Path("user")StringuserId);}
一個天真的實現ViewModel可以直接調用Webservice來獲取數據并將其分配給用戶對象。即使它可行,您的應用程序也將難以維持。它給ViewModel類提供了太多的責任,這違背了前面提到的關注點分離原則。此外,ViewModel的范圍與一個Activity或一個Fragment生命周期相關聯,所以當生命周期完成時丟失所有的數據是一個糟糕的用戶體驗。相反,我們的ViewModel將這個工作委托給一個新的Repository模塊。
存儲庫模塊負責處理數據操作。他們提供了一個干凈的API到應用程序的其余部分。他們知道從何處獲取數據以及在更新數據時調用哪些API。您可以將它們視為不同數據源(持久模型,Web服務,緩存等)之間的中介。
UserRepository下面的類使用WebService獲取用戶數據項。
publicclassUserRepository{privateWebservicewebservice;// ...publicLiveDatagetUser(intuserId){// This is not an optimal implementation, we'll fix it belowfinalMutableLiveDatadata=newMutableLiveData<>();
webservice
.getUser(userId).enqueue(newCallback(){@OverridepublicvoidonResponse(Callcall,Responseresponse){// error case is left out for brevity
data
.setValue(response.body());}});returndata;}}
即使存儲庫模塊看起來不必要,它也是一個重要的目的。它從應用程序的其余部分提取數據源。現在我們的ViewModel不知道數據是由Webservice哪個取得的,這意味著我們可以根據需要將它交換為其他的實現。
注意:為了簡單起見,我們忽略了網絡錯誤的情況。有關公開錯誤和加載狀態的替代實現,請參閱附錄:公開網絡狀態。
管理組件之間的依賴關系:
UserRepository上面的類需要一個工作的實例Webservice。它可以簡單地創建它,但要做到這一點,它也需要知道Webservice類的依賴關系來構造它。這會使代碼復雜化和復制(例如,每個需要Webservice實例的類都需要知道如何用它的依賴來構造它)。另外,UserRepository可能不是唯一需要的類Webservice。如果每個班級創建一個新的WebService,這將是非常重的資源。
有兩種模式可以用來解決這個問題:
依賴注入:依賴注入允許類在不構造它們的情況下定義它們的依賴關系。在運行時,另一個類負責提供這些依賴關系。我們推薦Google的Dagger 2庫在Android應用程序中實現依賴注入。Dagger 2通過遍歷依賴關系樹來自動構造對象,并為依賴關系提供編譯時間保證。
服務定位器:服務定位器提供了一個注冊表,類可以獲得它們的依賴而不是構建它們。實現起來比依賴注入(DI)更容易,所以如果你不熟悉DI,可以使用Service Locator。
這些模式允許您擴展代碼,因為它們提供了用于管理依賴關系的清晰模式,無需復制代碼或增加復雜性。他們兩人也允許交換實現測試;這是使用它們的主要好處之一。
在這個例子中,我們將使用Dagger 2來管理依賴關系。
連接ViewModel和存儲庫
現在我們修改我們UserProfileViewModel的存儲庫。
publicclassUserProfileViewModelextendsViewModel{privateLiveDatauser;privateUserRepositoryuserRepo;
@Inject// UserRepository parameter is provided by Dagger 2publicUserProfileViewModel(UserRepositoryuserRepo){this.userRepo=userRepo;}
publicvoidinit(StringuserId){if(this.user!=null){// ViewModel is created per Fragment so// we know the userId won't changereturn;}
user
=userRepo.getUser(userId);}
publicLiveDatagetUser(){returnthis.user;}}
緩存數據
上面的存儲庫實現對抽象調用Web服務是有好處的,但是因為它只依賴于一個數據源,所以它不是很實用。
UserRepository上面的實現的問題是,在獲取數據之后,它不保存在任何地方。如果用戶離開UserProfileFragment并返回,應用程序將重新獲取數據。這是不好的,原因有兩個:浪費寶貴的網絡帶寬并強制用戶等待新的查詢完成。為了解決這個問題,我們將添加一個新的數據源,我們UserRepository將緩存User內存中的對象。
@Singleton// informs Dagger that this class should be constructed oncepublicclassUserRepository{privateWebservicewebservice;// simple in memory cache, details omitted for brevityprivateUserCacheuserCache;publicLiveDatagetUser(StringuserId){LiveDatacached=userCache.get(userId);if(cached!=null){returncached;}
finalMutableLiveDatadata=newMutableLiveData<>();
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(newCallback(){@OverridepublicvoidonResponse(Callcall,Responseresponse){
data
.setValue(response.body());}});returndata;}}
堅持數據
在我們當前的實現中,如果用戶旋轉屏幕或離開并返回到應用程序,則現有UI將立即可見,因為存儲庫從內存中高速緩存中檢索數據。但是,如果用戶離開應用程序,并在Android操作系統殺死該進程后數小時后回來,會發生什么?
在目前的實施中,我們將需要從網絡上重新獲取數據。這不僅是一個糟糕的用戶體驗,而且會浪費,因為它會使用移動數據重新獲取相同的數據。您可以簡單地通過緩存Web請求來解決這個問題,但是會產生新的問題。如果相同的用戶數據顯示出另一種類型的請求(例如,獲取朋友列表),會發生什么情況?那么你的應用程序可能會顯示不一致的數據,這是一個混亂的用戶體驗充其量。例如,由于好友列表請求和用戶請求可以在不同的時間執行,所以相同用戶的數據可能會以不同的方式顯示。您的應用需要合并它們以避免顯示不一致的數據。
處理這個問題的正確方法是使用持久模型。這是Room持久性庫來救援的地方。
房間是一個對象映射庫,提供本地數據持久性和最小的樣板代碼。在編譯時,它會根據模式驗證每個查詢,以便斷開的SQL查詢導致編譯時錯誤,而不是運行時失敗。會議室抽象出一些使用原始SQL表和查詢的底層實現細節。它還允許觀察對數據庫數據(包括集合和連接查詢)的更改,通過LiveData對象公開這些更改。另外,它明確定義了解決常見問題的線程約束,例如訪問主線程上的存儲。
注意:如果您的應用程序已經使用另一個持久性解決方案(如SQLite對象關系映射(ORM)),則不需要使用Room替換現有的解決方案。但是,如果您正在編寫新的應用程序或重構現有的應用程序,我們建議使用Room來保存應用程序的數據。這樣,您可以利用庫的抽象和查詢驗證功能。
要使用Room,我們需要定義我們的本地模式。首先,注釋User該類以@Entity將其標記為數據庫中的表。
@EntityclassUser{@PrimaryKeyprivateintid;privateStringname;privateStringlastName;// getters and setters for fields}
然后,通過擴展RoomDatabase您的應用程序來創建一個數據庫類:
@Database(entities={User.class},version=1)publicabstractclassMyDatabaseextendsRoomDatabase{}
注意這MyDatabase是抽象的。房間自動提供一個實施。有關詳細信息,請參見房間文檔
現在我們需要一種將用戶數據插入數據庫的方法。為此,我們將創建一個數據訪問對象(DAO)。
@DaopublicinterfaceUserDao{@Insert(onConflict=REPLACE)voidsave(Useruser);@Query("SELECT * FROM user WHERE id = :userId")LiveDataload(StringuserId);}
然后,從我們的數據庫類中引用DAO。
@Database(entities={User.class},version=1)publicabstractclassMyDatabaseextendsRoomDatabase{publicabstractUserDaouserDao();}
請注意,該load方法返回一個LiveData。房間知道數據庫何時被修改,當數據改變時它會自動通知所有活動的觀察者。因為它使用的是LiveData,所以這將是有效的,因為只有至少有一個活動的觀察者才會更新數據。
注意:房間根據表格修改檢查失效,這意味著它可能發送誤報通知。
現在我們可以修改我們UserRepository來合并房間數據源。
@SingletonpublicclassUserRepository{privatefinalWebservicewebservice;privatefinalUserDaouserDao;privatefinalExecutorexecutor;
@InjectpublicUserRepository(Webservicewebservice,UserDaouserDao,Executorexecutor){this.webservice=webservice;this.userDao=userDao;this.executor=executor;}
publicLiveDatagetUser(StringuserId){
refreshUser
(userId);// return a LiveData directly from the database.returnuserDao.load(userId);}
privatevoidrefreshUser(finalStringuserId){
executor
.execute(()->{// running in a background thread// check if user was fetched recentlybooleanuserExists=userDao.hasUser(FRESH_TIMEOUT);if(!userExists){// refresh the dataResponseresponse=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。
現在我們的代碼是完整的。如果用戶以后回到相同的用戶界面,他們會立即看到用戶信息,因為我們堅持了。同時,如果數據陳舊,我們的倉庫將在后臺更新數據。當然,根據您的使用情況,如果數據太舊,您可能不希望顯示持久數據。
在一些使用情況下,如拉到刷新,UI顯示用戶是否正在進行網絡操作是非常重要的。將UI操作與實際數據分開是一種很好的做法,因為它可能因各種原因而更新(例如,如果我們獲取朋友列表,可能會再次觸發同一用戶觸發LiveData更新)。從用戶界面的角度來看,有一個請求在飛行的事實只是另一個數據點,類似于任何其他數據(如User對象)。
這個用例有兩個常見的解決方案:
更改getUser為返回包含網絡操作狀態的LiveData。附錄中提供了一個示例實現:公開網絡狀態部分。
在存儲庫類中提供另一個可以返回用戶刷新狀態的公共函數。如果只想響應顯式的用戶操作(如拉到刷新)來顯示網絡狀態,則此選項更好。
單一的事實來源
不同的REST API端點通常返回相同的數據。例如,如果我們的后端擁有另一個返回朋友列表的端點,則同一個用戶對象可能來自兩個不同的API端點,也許粒度不同。如果原樣UserRepository返回Webservice請求的響應,我們的UI可能會顯示不一致的數據,因為在這些請求之間數據可能在服務器端發生更改。這就是為什么在UserRepository實現中,Web服務回調只是將數據保存到數據庫中。然后,對數據庫的更改將觸發活動LiveData對象上的回調。
在這個模型中,數據庫充當真相的單一來源,應用程序的其他部分通過存儲庫訪問它。無論您使用磁盤緩存,我們都建議您的存儲庫將數據源指定為應用程序其余部分的單一來源。
測試
我們已經提到分離的好處之一就是可測試性。讓我們看看我們如何測試每個代碼模塊。
用戶界面和交互:這將是唯一一次你需要一個Android用戶界面工具測試。測試UI代碼的最好方法是創建一個Espresso測試。您可以創建片段并為其提供一個模擬的ViewModel。由于該片段只與ViewModel交談,所以嘲笑它將足以完全測試這個UI。
ViewModel:可以使用JUnit測試來測試ViewModel。你只需要嘲笑UserRepository測試它。
UserRepository:您也可以UserRepository使用JUnit測試來測試。你需要嘲笑Webservice和DAO。您可以測試它是否進行正確的Web服務調用,將結果保存到數據庫中,如果數據已緩存且最新,則不會發出任何不必要的請求。由于這兩個Webservice和UserDao的界面,你可以嘲笑他們或創建更復雜的測試案例假冒實現..
UserDao:測試DAO類的推薦方法是使用儀器測試。由于這些儀器測試不需要任何用戶界面,他們仍然會運行得很快。對于每個測試,您可以創建一個內存數據庫,以確保測試沒有任何副作用(如更改磁盤上的數據庫文件)。
Room也允許指定數據庫的實現,所以你可以通過提供它的JUnit實現來測試它SupportSQLiteOpenHelper。通常不建議使用這種方法,因為設備上運行的SQLite版本可能與主機上的SQLite版本不同。
Webservice:使測試獨立于外部是很重要的,所以即使你的Webservice測試也應該避免對后端進行網絡調用。有很多圖書館可以幫助你。例如,MockWebServer是一個偉大的庫,可以幫助您為測試創建一個假的本地服務器。
測試工件體系結構組件提供了一個Maven工件來控制其后臺線程。在android.arch.core:core-testing神器內部,有2個JUnit規則:
InstantTaskExecutorRule:此規則可用于強制架構組件立即在調用線程上執行任何后臺操作。
CountingTaskExecutorRule:此規則可用于檢測測試,以等待體系結構組件的后臺操作或將其作為閑置資源連接到Espresso。
最終的體系結構
下圖顯示了我們推薦的體系結構中的所有模塊以及它們如何相互交互:
指導原則
編程是一個創造性的領域,構建Android應用程序不是一個例外。解決問題的方法有很多種,可以在多個活動或片段之間傳遞數據,檢索遠程數據并將其保存在本地以進行脫機模式,也可以使用許多其他常見應用程序遇到的情況。
雖然以下建議不是強制性的,但是我們的經驗是,遵循這些建議將使您的代碼基礎更加健壯,可測試和可維護。
您在清單中定義的入口點(活動,服務,廣播接收器等)不是數據的來源。相反,他們只應該協調與該入口點相關的數據子集。由于每個應用程序組件的壽命相當短,這取決于用戶與設備的交互以及運行時的整體當前運行狀況,因此您不希望這些入口點中的任何一個成為數據源。
無情地在應用程序的各個模塊之間創建明確界定的責任。例如,不要將從網絡加載數據的代碼跨代碼庫中的多個類或包傳播。同樣,不要把不相關的職責 - 比如數據緩存和數據綁定 - 放到同一個類中。
盡可能少地從每個模塊公開。不要試圖創建“只有那一個”的快捷方式,從一個模塊公開內部實現細節。您可能在短期內獲得一些時間,但隨著您的代碼庫的發展,您將多次支付技術債務。
在定義模塊之間的交互時,請考慮如何使每個模塊獨立地進行測試。例如,如果有一個定義良好的API從網絡中獲取數據,將會更容易測試將數據保存在本地數據庫中的模塊。相反,如果將這兩個模塊的邏輯混合在一起,或者在整個代碼庫中撒上網絡代碼,那么要測試就更加困難了。
你的應用程序的核心是什么讓它從其他中脫穎而出。不要花費時間重復發明輪子,或者一次又一次地寫出相同的樣板代碼。相反,將精力集中在讓您的應用獨特的東西上,讓Android Architecture組件和其他推薦的庫處理重復的樣板。
堅持盡可能多的相關和新鮮的數據,以便您的應用程序在設備處于離線模式時可用。雖然您可以享受持續高速的連接,但用戶可能不會。
您的存儲庫應該指定一個數據源作為單一的事實來源。無論何時您的應用程序需要訪問這些數據,都應該始終從單一的事實源頭開始。有關更多信息,請參閱單一來源的真相。
附錄:揭露網絡狀態
在上面推薦的應用程序體系結構部分,我們故意省略網絡錯誤和加載狀態,以保持樣本簡單。在本節中,我們演示一種使用Resource類來公開網絡狀態的方法來封裝數據及其狀態。
以下是一個示例實現:
//a generic class that describes a data with a statuspublicclassResource{@NonNullpublicfinalStatusstatus;@NullablepublicfinalT data;@NullablepublicfinalStringmessage;privateResource(@NonNullStatusstatus,@NullableT data,@NullableStringmessage){this.status=status;this.data=data;this.message=message;}
publicstaticResourcesuccess(@NonNullT data){returnnewResource<>(SUCCESS,data,null);}
publicstaticResourceerror(Stringmsg,@NullableT data){returnnewResource<>(ERROR,data,msg);}
publicstaticResourceloading(@NullableT data){returnnewResource<>(LOADING,data,null);}}
因為在從磁盤顯示數據時從網絡加載數據是一個常見的用例,我們將創建一個NetworkBoundResource可以在多個地方重復使用的幫助類。以下是決策樹NetworkBoundResource:
它通過觀察資源的數據庫開始。當條目從數據庫中第一次加載時,NetworkBoundResource檢查結果是否足夠好以便分派和/或從網絡中獲取。請注意,這兩種情況可能同時發生,因為您可能希望在從網絡更新緩存數據時顯示緩存的數據。
如果網絡呼叫成功完成,則將響應保存到數據庫中并重新初始化流。如果網絡請求失敗,我們直接發送失敗。
注意:在將新數據保存到磁盤之后,我們會重新初始化數據庫中的數據流,但通常我們不需要這樣做,因為數據庫將分派更改。另一方面,依靠數據庫來調度變化將依賴于不好的副作用,因為如果數據沒有變化,數據庫可以避免調度變化,那么它可能會中斷。我們也不希望發送從網絡到達的結果,因為這將違背單一的事實來源(也許在數據庫中有觸發器會改變保存的值)。我們也不想SUCCESS沒有新的數據,因為它會向客戶發送錯誤的信息。
以下是NetworkBoundResource班級為其子女提供的公共API:
// ResultType: Type for the Resource data// RequestType: Type for the API responsepublicabstractclassNetworkBoundResource{// Called to save the result of the API response into the database@WorkerThreadprotectedabstractvoidsaveCallResult(@NonNullRequestTypeitem);
// Called with the data in the database to decide whether it should be// fetched from the network.@MainThreadprotectedabstractbooleanshouldFetch(@NullableResultTypedata);
// Called to get the cached data from the database@NonNull@MainThreadprotectedabstractLiveDataloadFromDb();
// Called to create the API call.@NonNull@MainThreadprotectedabstractLiveData>createCall();
// Called when the fetch fails. The child class may want to reset components// like rate limiter.@MainThreadprotectedvoidonFetchFailed(){}
// returns a LiveData that represents the resource, implemented// in the base class.publicfinalLiveData>getAsLiveData();}
請注意,上面的類定義了兩個類型參數(ResultType,RequestType),因為從API返回的數據類型可能與本地使用的數據類型不匹配。
另請注意,上面的代碼ApiResponse用于網絡請求。ApiResponse是一個簡單的Retrofit2.Call類包裝,將其響應轉換為LiveData。
以下是該NetworkBoundResource課程的其余部分:
publicabstractclassNetworkBoundResource{privatefinalMediatorLiveData>result=newMediatorLiveData<>();
@MainThreadNetworkBoundResource(){
result
.setValue(Resource.loading(null));LiveDatadbSource=loadFromDb();
result
.addSource(dbSource,data->{
result
.removeSource(dbSource);if(shouldFetch(data)){
fetchFromNetwork
(dbSource);}else{
result
.addSource(dbSource,
newData
->result.setValue(Resource.success(newData)));}});}
privatevoidfetchFromNetwork(finalLiveDatadbSource){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 ConstantConditionsif(response.isSuccessful()){
saveResultAndReInit
(response);}else{
onFetchFailed
();
result
.addSource(dbSource,
newData
->result.setValue(Resource.error(response.errorMessage,newData)));}});}
@MainThreadprivatevoidsaveResultAndReInit(ApiResponseresponse){newAsyncTask(){
@OverrideprotectedVoiddoInBackground(Void...voids){
saveCallResult
(response.body);returnnull;}
@OverrideprotectedvoidonPostExecute(VoidaVoid){// 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();}
publicfinalLiveData>getAsLiveData(){returnresult;}}
現在,我們可以使用它NetworkBoundResource來User在存儲庫中寫入我們的磁盤和網絡綁定實現。
classUserRepository{Webservicewebservice;UserDaouserDao;
publicLiveData>loadUser(finalStringuserId){returnnewNetworkBoundResource(){@OverrideprotectedvoidsaveCallResult(@NonNullUseritem){
userDao
.insert(item);}
@OverrideprotectedbooleanshouldFetch(@NullableUserdata){returnrateLimiter.canFetch(userId)&&(data==null||!isFresh(data));}
@NonNull@OverrideprotectedLiveDataloadFromDb(){returnuserDao.load(userId);}
@NonNull@OverrideprotectedLiveData>createCall(){returnwebservice.getUser(userId);}}.getAsLiveData();}}