本文翻譯開(kāi)發(fā)者官網(wǎng)
App體系結(jié)構(gòu)指南
本指南適用于那些過(guò)去構(gòu)建應(yīng)用程序的基礎(chǔ)知識(shí)的開(kāi)發(fā)人員,現(xiàn)在想知道構(gòu)建強(qiáng)大的生產(chǎn)質(zhì)量應(yīng)用程序的最佳實(shí)踐和建議的體系結(jié)構(gòu)梅桩。
注意:本指南假定讀者熟悉Android框架。如果您不熟悉應(yīng)用程序開(kāi)發(fā)余黎,請(qǐng)查看入門(mén)培訓(xùn)系列航邢,其中包含本指南的必備主題躏升。
應(yīng)用開(kāi)發(fā)者面臨的常見(jiàn)問(wèn)題
與傳統(tǒng)的桌面應(yīng)用程序不同,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多节榜,在大多數(shù)情況下羡玛,它們只有一個(gè)啟動(dòng)快捷方式的入口點(diǎn),并且可以作為一個(gè)整體進(jìn)程運(yùn)行宗苍。一個(gè)典型的Android應(yīng)用程序是由多個(gè)應(yīng)用程序組件構(gòu)成的稼稿,包括活動(dòng)薄榛,片段,服務(wù)让歼,內(nèi)容提供者和廣播接收器敞恋。
大多數(shù)這些應(yīng)用程序組件都是在Android操作系統(tǒng)使用的應(yīng)用程序清單中聲明的,以決定如何將您的應(yīng)用程序與其設(shè)備的整體用戶(hù)體驗(yàn)相集成谋右。雖然如前所述硬猫,桌面應(yīng)用程序傳統(tǒng)上是以整體的方式運(yùn)行的,但正確編寫(xiě)的Android應(yīng)用程序需要更加靈活改执,因?yàn)橛脩?hù)可以通過(guò)設(shè)備上的不同應(yīng)用程序進(jìn)行編程啸蜜,不斷切換流程和任務(wù)。
例如辈挂,請(qǐng)考慮在您最喜愛(ài)的社交網(wǎng)絡(luò)應(yīng)用程序中分享照片時(shí)會(huì)發(fā)生什么情況衬横。該應(yīng)用程序觸發(fā)Android操作系統(tǒng)啟動(dòng)相機(jī)應(yīng)用程序來(lái)處理請(qǐng)求的相機(jī)意圖。此時(shí)终蒂,用戶(hù)離開(kāi)了社交網(wǎng)絡(luò)應(yīng)用冕香,但他們的體驗(yàn)是無(wú)縫的坛善。相機(jī)應(yīng)用程序又可能觸發(fā)其他意圖鹅士,例如啟動(dòng)文件選擇器窃款,該文件選擇器可以啟動(dòng)另一個(gè)應(yīng)用程序隘膘。最終用戶(hù)回到社交網(wǎng)絡(luò)應(yīng)用程序并分享照片脆丁。此外惨缆,用戶(hù)在這個(gè)過(guò)程的任何時(shí)候都可能被電話打斷寥裂,并在打完電話后回來(lái)分享照片哈垢。
在Android中早龟,這種應(yīng)用程序跳轉(zhuǎn)行為很常見(jiàn)惫霸,所以您的應(yīng)用程序必須正確處理這些流程。請(qǐng)記住葱弟,移動(dòng)設(shè)備是資源受限壹店,所以在任何時(shí)候,操作系統(tǒng)可能需要?dú)⑺酪恍?yīng)用程序芝加,以騰出空間給新的硅卢。
所有這一切的關(guān)鍵是,您的應(yīng)用程序組件可以單獨(dú)和無(wú)序地啟動(dòng)藏杖,并可以在任何時(shí)候由用戶(hù)或系統(tǒng)銷(xiāo)毀将塑。由于應(yīng)用程序組件是短暫的,它們的生命周期(創(chuàng)建和銷(xiāo)毀時(shí))不在您的控制之下蝌麸,因此您不應(yīng)該在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用程序數(shù)據(jù)或狀態(tài)点寥,并且應(yīng)用程序組件不應(yīng)相互依賴(lài)。
共同的建筑原則
如果您不能使用應(yīng)用程序組件來(lái)存儲(chǔ)應(yīng)用程序數(shù)據(jù)和狀態(tài)来吩,應(yīng)該如何構(gòu)建應(yīng)用程序敢辩?
你應(yīng)該關(guān)注的最重要的事情是在你的應(yīng)用程序中分離關(guān)注點(diǎn)蔽莱。將所有的代碼寫(xiě)入一個(gè)Activity或一個(gè)常見(jiàn)的錯(cuò)誤Fragment。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類(lèi)中戚长。盡可能保持精簡(jiǎn)可以避免許多生命周期相關(guān)的問(wèn)題盗冷。不要忘記,你不擁有這些類(lèi)历葛,它們只是體現(xiàn)操作系統(tǒng)和你的應(yīng)用程序之間的契約的膠水類(lèi)。Android操作系統(tǒng)可能會(huì)隨時(shí)根據(jù)用戶(hù)交互或其他因素(如低內(nèi)存)來(lái)銷(xiāo)毀它們嘀略。最好最大限度地減少對(duì)他們的依賴(lài)恤溶,以提供可靠的用戶(hù)體驗(yàn)。
第二個(gè)重要的原則是你應(yīng)該從一個(gè)模型驅(qū)動(dòng)你的UI帜羊,最好是一個(gè)持久模型咒程。持久性是理想的,原因有兩個(gè):如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源讼育,則您的用戶(hù)不會(huì)丟失數(shù)據(jù)帐姻,即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,您的應(yīng)用程序也將繼續(xù)工作奶段。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件饥瓷。它們獨(dú)立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問(wèn)題是隔離的痹籍。保持簡(jiǎn)單的UI代碼和免費(fèi)的應(yīng)用程序邏輯呢铆,使管理更容易。將您的應(yīng)用程序放在具有明確定義的管理數(shù)據(jù)責(zé)任的模型類(lèi)上蹲缠,將使它們可測(cè)試棺克,并使您的應(yīng)用程序保持一致。
推薦的應(yīng)用架構(gòu)
在本節(jié)中线定,我們將演示如何通過(guò)使用用例來(lái)構(gòu)建使用體系結(jié)構(gòu)組件的應(yīng)用程序娜谊。
注意:不可能有一種編寫(xiě)應(yīng)用程序的方法,這對(duì)每種情況都是最好的斤讥。這就是說(shuō)纱皆,這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個(gè)很好的起點(diǎn)。如果您已經(jīng)有了編寫(xiě)Android應(yīng)用的好方法芭商,則不需要更改抹剩。
想象一下,我們正在構(gòu)建一個(gè)顯示用戶(hù)配置文件的用戶(hù)界面蓉坎。該用戶(hù)配置文件將使用REST API從我們自己的私人后端獲取澳眷。
構(gòu)建用戶(hù)界面
UI將由一個(gè)片段UserProfileFragment.java及其相應(yīng)的布局文件組成user_profile_layout.xml。
為了驅(qū)動(dòng)用戶(hù)界面蛉艾,我們的數(shù)據(jù)模型需要保存兩個(gè)數(shù)據(jù)元素钳踊。
用戶(hù)ID:用戶(hù)的標(biāo)識(shí)符衷敌。最好使用片段參數(shù)將此信息傳遞到片段中。如果Android操作系統(tǒng)破壞您的進(jìn)程拓瞪,這些信息將被保留缴罗,以便在您的應(yīng)用下次重新啟動(dòng)時(shí)可用。
用戶(hù)對(duì)象:保存用戶(hù)數(shù)據(jù)的POJO祭埂。
我們將創(chuàng)建一個(gè)UserProfileViewModel基于ViewModel的類(lèi)來(lái)保存這些信息面氓。
甲視圖模型提供了一個(gè)特定的UI組件中的數(shù)據(jù),如一個(gè)片段或活性蛆橡,和處理與數(shù)據(jù)處理的部分業(yè)務(wù)舌界,如主叫其他組件加載數(shù)據(jù)或轉(zhuǎn)發(fā)的用戶(hù)修改的通信。ViewModel不知道視圖泰演,并且不受配置更改的影響呻拌,例如由于旋轉(zhuǎn)而重新創(chuàng)建活動(dòng)。
現(xiàn)在我們有3個(gè)文件睦焕。
user_profile.xml:屏幕的UI定義藐握。
UserProfileViewModel.java:為UI準(zhǔn)備數(shù)據(jù)的類(lèi)。
UserProfileFragment.java:在ViewModel中顯示數(shù)據(jù)并對(duì)用戶(hù)交互作出反應(yīng)的UI控制器垃喊。
下面是我們的開(kāi)始的實(shí)現(xiàn)(布局文件為簡(jiǎn)單起見(jiàn)被省略):
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);}}
現(xiàn)在猾普,我們有這三個(gè)代碼模塊,我們?nèi)绾芜B接它們本谜?畢竟抬闷,當(dāng)ViewModel的用戶(hù)字段被設(shè)置,我們需要一種方式來(lái)通知用戶(hù)界面耕突。這是LiveData類(lèi)的地方笤成。
LiveData是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData對(duì)象的更改眷茁,而不會(huì)在它們之間創(chuàng)建明確的和嚴(yán)格的依賴(lài)關(guān)系路徑炕泳。LiveData還尊重您的應(yīng)用程序組件(活動(dòng),片段上祈,服務(wù))的生命周期狀態(tài)培遵,并做正確的事情來(lái)防止對(duì)象泄漏,使您的應(yīng)用程序不消耗更多的內(nèi)存登刺。
注意:如果您已經(jīng)在使用類(lèi)似RxJava或Agera的庫(kù)籽腕,則可以繼續(xù)使用它們而不是LiveData。但是纸俭,當(dāng)您使用它們或其他方法時(shí)皇耗,請(qǐng)確保正確處理生命周期,以便在相關(guān)的LifecycleOwner停止時(shí)停止數(shù)據(jù)流揍很,并在銷(xiāo)毀LifecycleOwner時(shí)銷(xiāo)毀數(shù)據(jù)流郎楼。您還可以添加android.arch.lifecycle:reactivestreams工件以將LiveData與另一個(gè)反應(yīng)流庫(kù)(例如RxJava2)一起使用万伤。
現(xiàn)在我們用a替換User字段,以便在數(shù)據(jù)更新時(shí)通知片段呜袁。重要的是敌买,它是生命周期感知,并將自動(dòng)清理引用時(shí)阶界,不再需要虹钮。UserProfileViewModelLiveDataLiveData
公共類(lèi)UserProfileViewModel擴(kuò)展ViewModel{...私人用戶(hù)的用戶(hù);私人LiveData<用戶(hù)>用戶(hù);publicLiveDatagetUser(){returnuser;}}
現(xiàn)在我們修改UserProfileFragment觀察數(shù)據(jù)并更新UI。
@OverridepublicvoidonActivityCreated(@NullableBundlesavedInstanceState){super.onActivityCreated(savedInstanceState);
viewModel
.getUser().observe(this,user->{// update UI});}
每次更新用戶(hù)數(shù)據(jù)時(shí)膘融,都會(huì)調(diào)用onChanged回調(diào)芙粱,并刷新UI。
如果您熟悉使用可觀察回調(diào)的其他庫(kù)托启,您可能已經(jīng)意識(shí)到宅倒,我們不必重寫(xiě)片段的onStop()方法來(lái)停止觀察數(shù)據(jù)攘宙。這對(duì)于LiveData來(lái)說(shuō)是不必要的屯耸,因?yàn)樗巧芷诟兄模@意味著它不會(huì)調(diào)用回調(diào)蹭劈,除非片段處于活動(dòng)狀態(tài)(已接收onStart()但未接收onStop())疗绣。當(dāng)數(shù)據(jù)片段收到時(shí),LiveData也會(huì)自動(dòng)移除觀察者onDestroy()铺韧。
我們也沒(méi)有做任何特殊的處理配置變化(例如多矮,用戶(hù)旋轉(zhuǎn)屏幕)。當(dāng)配置改變時(shí)哈打,ViewModel會(huì)自動(dòng)恢復(fù)塔逃,所以一旦新的片段生效,它將接收到相同的ViewModel實(shí)例料仗,并且回調(diào)將被當(dāng)前數(shù)據(jù)立即調(diào)用湾盗。這就是ViewModel不能直接引用Views的原因。他們可以超越View的生命周期立轧。請(qǐng)參閱ViewModel的生命周期格粪。
正在提取數(shù)據(jù)
現(xiàn)在我們已經(jīng)將ViewModel連接到了片段,但是ViewModel如何獲取用戶(hù)數(shù)據(jù)呢氛改?在這個(gè)例子中帐萎,我們假設(shè)我們的后端提供了一個(gè)REST API。我們將使用Retrofit庫(kù)來(lái)訪問(wèn)我們的后端胜卤,盡管您可以自由使用不同的庫(kù)來(lái)達(dá)到同樣的目的疆导。
以下是我們Webservice與后端進(jìn)行通信的改造:
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);}
一個(gè)天真的實(shí)現(xiàn)ViewModel可以直接調(diào)用Webservice來(lái)獲取數(shù)據(jù)并將其分配給用戶(hù)對(duì)象。即使它可行葛躏,您的應(yīng)用程序也將難以維持是鬼。它給ViewModel類(lèi)提供了太多的責(zé)任肤舞,這違背了前面提到的關(guān)注點(diǎn)分離原則。此外均蜜,ViewModel的范圍與一個(gè)Activity或一個(gè)Fragment生命周期相關(guān)聯(lián)李剖,所以當(dāng)生命周期完成時(shí)丟失所有的數(shù)據(jù)是一個(gè)糟糕的用戶(hù)體驗(yàn)。相反囤耳,我們的ViewModel將這個(gè)工作委托給一個(gè)新的Repository模塊篙顺。
存儲(chǔ)庫(kù)模塊負(fù)責(zé)處理數(shù)據(jù)操作。他們提供了一個(gè)干凈的API到應(yīng)用程序的其余部分充择。他們知道從何處獲取數(shù)據(jù)以及在更新數(shù)據(jù)時(shí)調(diào)用哪些API德玫。您可以將它們視為不同數(shù)據(jù)源(持久模型,Web服務(wù)椎麦,緩存等)之間的中介宰僧。
UserRepository下面的類(lèi)使用WebService獲取用戶(hù)數(shù)據(jù)項(xiàng)。
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;}}
即使存儲(chǔ)庫(kù)模塊看起來(lái)不必要观挎,它也是一個(gè)重要的目的琴儿。它從應(yīng)用程序的其余部分提取數(shù)據(jù)源。現(xiàn)在我們的ViewModel不知道數(shù)據(jù)是由Webservice哪個(gè)取得的嘁捷,這意味著我們可以根據(jù)需要將它交換為其他的實(shí)現(xiàn)造成。
注意:為了簡(jiǎn)單起見(jiàn),我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況雄嚣。有關(guān)公開(kāi)錯(cuò)誤和加載狀態(tài)的替代實(shí)現(xiàn)晒屎,請(qǐng)參閱附錄:公開(kāi)網(wǎng)絡(luò)狀態(tài)。
管理組件之間的依賴(lài)關(guān)系:
UserRepository上面的類(lèi)需要一個(gè)工作的實(shí)例Webservice缓升。它可以簡(jiǎn)單地創(chuàng)建它鼓鲁,但要做到這一點(diǎn),它也需要知道Webservice類(lèi)的依賴(lài)關(guān)系來(lái)構(gòu)造它港谊。這會(huì)使代碼復(fù)雜化和復(fù)制(例如骇吭,每個(gè)需要Webservice實(shí)例的類(lèi)都需要知道如何用它的依賴(lài)來(lái)構(gòu)造它)。另外封锉,UserRepository可能不是唯一需要的類(lèi)Webservice绵跷。如果每個(gè)班級(jí)創(chuàng)建一個(gè)新的WebService,這將是非常重的資源成福。
有兩種模式可以用來(lái)解決這個(gè)問(wèn)題:
依賴(lài)注入:依賴(lài)注入允許類(lèi)在不構(gòu)造它們的情況下定義它們的依賴(lài)關(guān)系碾局。在運(yùn)行時(shí),另一個(gè)類(lèi)負(fù)責(zé)提供這些依賴(lài)關(guān)系奴艾。我們推薦Google的Dagger 2庫(kù)在Android應(yīng)用程序中實(shí)現(xiàn)依賴(lài)注入净当。Dagger 2通過(guò)遍歷依賴(lài)關(guān)系樹(shù)來(lái)自動(dòng)構(gòu)造對(duì)象,并為依賴(lài)關(guān)系提供編譯時(shí)間保證。
服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊(cè)表像啼,類(lèi)可以獲得它們的依賴(lài)而不是構(gòu)建它們俘闯。實(shí)現(xiàn)起來(lái)比依賴(lài)注入(DI)更容易,所以如果你不熟悉DI忽冻,可以使用Service Locator真朗。
這些模式允許您擴(kuò)展代碼,因?yàn)樗鼈兲峁┝擞糜诠芾硪蕾?lài)關(guān)系的清晰模式僧诚,無(wú)需復(fù)制代碼或增加復(fù)雜性遮婶。他們兩人也允許交換實(shí)現(xiàn)測(cè)試;這是使用它們的主要好處之一。
在這個(gè)例子中湖笨,我們將使用Dagger 2來(lái)管理依賴(lài)關(guān)系旗扑。
連接ViewModel和存儲(chǔ)庫(kù)
現(xiàn)在我們修改我們UserProfileViewModel的存儲(chǔ)庫(kù)。
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;}}
緩存數(shù)據(jù)
上面的存儲(chǔ)庫(kù)實(shí)現(xiàn)對(duì)抽象調(diào)用Web服務(wù)是有好處的慈省,但是因?yàn)樗灰蕾?lài)于一個(gè)數(shù)據(jù)源臀防,所以它不是很實(shí)用。
UserRepository上面的實(shí)現(xiàn)的問(wèn)題是边败,在獲取數(shù)據(jù)之后袱衷,它不保存在任何地方。如果用戶(hù)離開(kāi)UserProfileFragment并返回放闺,應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)祟昭。這是不好的缕坎,原因有兩個(gè):浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬并強(qiáng)制用戶(hù)等待新的查詢(xún)完成怖侦。為了解決這個(gè)問(wèn)題,我們將添加一個(gè)新的數(shù)據(jù)源谜叹,我們UserRepository將緩存User內(nèi)存中的對(duì)象匾寝。
@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;}}
堅(jiān)持?jǐn)?shù)據(jù)
在我們當(dāng)前的實(shí)現(xiàn)中,如果用戶(hù)旋轉(zhuǎn)屏幕或離開(kāi)并返回到應(yīng)用程序荷腊,則現(xiàn)有UI將立即可見(jiàn)艳悔,因?yàn)榇鎯?chǔ)庫(kù)從內(nèi)存中高速緩存中檢索數(shù)據(jù)。但是女仰,如果用戶(hù)離開(kāi)應(yīng)用程序猜年,并在Android操作系統(tǒng)殺死該進(jìn)程后數(shù)小時(shí)后回來(lái),會(huì)發(fā)生什么疾忍?
在目前的實(shí)施中乔外,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)。這不僅是一個(gè)糟糕的用戶(hù)體驗(yàn)一罩,而且會(huì)浪費(fèi)杨幼,因?yàn)樗鼤?huì)使用移動(dòng)數(shù)據(jù)重新獲取相同的數(shù)據(jù)。您可以簡(jiǎn)單地通過(guò)緩存Web請(qǐng)求來(lái)解決這個(gè)問(wèn)題,但是會(huì)產(chǎn)生新的問(wèn)題差购。如果相同的用戶(hù)數(shù)據(jù)顯示出另一種類(lèi)型的請(qǐng)求(例如四瘫,獲取朋友列表),會(huì)發(fā)生什么情況欲逃?那么你的應(yīng)用程序可能會(huì)顯示不一致的數(shù)據(jù)找蜜,這是一個(gè)混亂的用戶(hù)體驗(yàn)充其量。例如稳析,由于好友列表請(qǐng)求和用戶(hù)請(qǐng)求可以在不同的時(shí)間執(zhí)行锹杈,所以相同用戶(hù)的數(shù)據(jù)可能會(huì)以不同的方式顯示。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)迈着。
處理這個(gè)問(wèn)題的正確方法是使用持久模型竭望。這是Room持久性庫(kù)來(lái)救援的地方。
房間是一個(gè)對(duì)象映射庫(kù)裕菠,提供本地?cái)?shù)據(jù)持久性和最小的樣板代碼咬清。在編譯時(shí),它會(huì)根據(jù)模式驗(yàn)證每個(gè)查詢(xún)奴潘,以便斷開(kāi)的SQL查詢(xún)導(dǎo)致編譯時(shí)錯(cuò)誤旧烧,而不是運(yùn)行時(shí)失敗。會(huì)議室抽象出一些使用原始SQL表和查詢(xún)的底層實(shí)現(xiàn)細(xì)節(jié)画髓。它還允許觀察對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢(xún))的更改掘剪,通過(guò)LiveData對(duì)象公開(kāi)這些更改。另外奈虾,它明確定義了解決常見(jiàn)問(wèn)題的線程約束夺谁,例如訪問(wèn)主線程上的存儲(chǔ)。
注意:如果您的應(yīng)用程序已經(jīng)使用另一個(gè)持久性解決方案(如SQLite對(duì)象關(guān)系映射(ORM))肉微,則不需要使用Room替換現(xiàn)有的解決方案匾鸥。但是,如果您正在編寫(xiě)新的應(yīng)用程序或重構(gòu)現(xiàn)有的應(yīng)用程序碉纳,我們建議使用Room來(lái)保存應(yīng)用程序的數(shù)據(jù)勿负。這樣,您可以利用庫(kù)的抽象和查詢(xún)驗(yàn)證功能劳曹。
要使用Room奴愉,我們需要定義我們的本地模式。首先铁孵,注釋User該類(lèi)以@Entity將其標(biāo)記為數(shù)據(jù)庫(kù)中的表锭硼。
@EntityclassUser{@PrimaryKeyprivateintid;privateStringname;privateStringlastName;// getters and setters for fields}
然后,通過(guò)擴(kuò)展RoomDatabase您的應(yīng)用程序來(lái)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)類(lèi):
@Database(entities={User.class},version=1)publicabstractclassMyDatabaseextendsRoomDatabase{}
注意這MyDatabase是抽象的库菲。房間自動(dòng)提供一個(gè)實(shí)施账忘。有關(guān)詳細(xì)信息,請(qǐng)參見(jiàn)房間文檔
現(xiàn)在我們需要一種將用戶(hù)數(shù)據(jù)插入數(shù)據(jù)庫(kù)的方法。為此鳖擒,我們將創(chuàng)建一個(gè)數(shù)據(jù)訪問(wèn)對(duì)象(DAO)溉浙。
@DaopublicinterfaceUserDao{@Insert(onConflict=REPLACE)voidsave(Useruser);@Query("SELECT * FROM user WHERE id = :userId")LiveDataload(StringuserId);}
然后,從我們的數(shù)據(jù)庫(kù)類(lèi)中引用DAO蒋荚。
@Database(entities={User.class},version=1)publicabstractclassMyDatabaseextendsRoomDatabase{publicabstractUserDaouserDao();}
請(qǐng)注意戳稽,該load方法返回一個(gè)LiveData。房間知道數(shù)據(jù)庫(kù)何時(shí)被修改期升,當(dāng)數(shù)據(jù)改變時(shí)它會(huì)自動(dòng)通知所有活動(dòng)的觀察者惊奇。因?yàn)樗褂玫氖?i>LiveData,所以這將是有效的播赁,因?yàn)橹挥兄辽儆幸粋€(gè)活動(dòng)的觀察者才會(huì)更新數(shù)據(jù)颂郎。
注意:房間根據(jù)表格修改檢查失效,這意味著它可能發(fā)送誤報(bào)通知容为。
現(xiàn)在我們可以修改我們UserRepository來(lái)合并房間數(shù)據(jù)源乓序。
@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());}});}}
請(qǐng)注意,盡管我們改變了數(shù)據(jù)來(lái)自于UserRepository坎背,我們并不需要改變我們UserProfileViewModel或UserProfileFragment替劈。這是抽象提供的靈活性绿语。這對(duì)于測(cè)試來(lái)說(shuō)也很棒写隶,因?yàn)槟憧梢訳serRepository在測(cè)試你的時(shí)候提供一個(gè)假的UserProfileViewModel。
現(xiàn)在我們的代碼是完整的捡遍。如果用戶(hù)以后回到相同的用戶(hù)界面懂更,他們會(huì)立即看到用戶(hù)信息眨业,因?yàn)槲覀儓?jiān)持了。同時(shí)膜蛔,如果數(shù)據(jù)陳舊坛猪,我們的倉(cāng)庫(kù)將在后臺(tái)更新數(shù)據(jù)脖阵。當(dāng)然皂股,根據(jù)您的使用情況,如果數(shù)據(jù)太舊命黔,您可能不希望顯示持久數(shù)據(jù)呜呐。
在一些使用情況下,如拉到刷新悍募,UI顯示用戶(hù)是否正在進(jìn)行網(wǎng)絡(luò)操作是非常重要的蘑辑。將UI操作與實(shí)際數(shù)據(jù)分開(kāi)是一種很好的做法,因?yàn)樗赡芤蚋鞣N原因而更新(例如坠宴,如果我們獲取朋友列表洋魂,可能會(huì)再次觸發(fā)同一用戶(hù)觸發(fā)LiveData更新)。從用戶(hù)界面的角度來(lái)看,有一個(gè)請(qǐng)求在飛行的事實(shí)只是另一個(gè)數(shù)據(jù)點(diǎn)副砍,類(lèi)似于任何其他數(shù)據(jù)(如User對(duì)象)衔肢。
這個(gè)用例有兩個(gè)常見(jiàn)的解決方案:
更改getUser為返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。附錄中提供了一個(gè)示例實(shí)現(xiàn):公開(kāi)網(wǎng)絡(luò)狀態(tài)部分豁翎。
在存儲(chǔ)庫(kù)類(lèi)中提供另一個(gè)可以返回用戶(hù)刷新?tīng)顟B(tài)的公共函數(shù)角骤。如果只想響應(yīng)顯式的用戶(hù)操作(如拉到刷新)來(lái)顯示網(wǎng)絡(luò)狀態(tài),則此選項(xiàng)更好心剥。
單一的事實(shí)來(lái)源
不同的REST API端點(diǎn)通常返回相同的數(shù)據(jù)邦尊。例如,如果我們的后端擁有另一個(gè)返回朋友列表的端點(diǎn)优烧,則同一個(gè)用戶(hù)對(duì)象可能來(lái)自?xún)蓚€(gè)不同的API端點(diǎn)蝉揍,也許粒度不同。如果原樣UserRepository返回Webservice請(qǐng)求的響應(yīng)畦娄,我們的UI可能會(huì)顯示不一致的數(shù)據(jù)疑苫,因?yàn)樵谶@些請(qǐng)求之間數(shù)據(jù)可能在服務(wù)器端發(fā)生更改。這就是為什么在UserRepository實(shí)現(xiàn)中纷责,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)中捍掺。然后,對(duì)數(shù)據(jù)庫(kù)的更改將觸發(fā)活動(dòng)LiveData對(duì)象上的回調(diào)再膳。
在這個(gè)模型中挺勿,數(shù)據(jù)庫(kù)充當(dāng)真相的單一來(lái)源,應(yīng)用程序的其他部分通過(guò)存儲(chǔ)庫(kù)訪問(wèn)它喂柒。無(wú)論您使用磁盤(pán)緩存不瓶,我們都建議您的存儲(chǔ)庫(kù)將數(shù)據(jù)源指定為應(yīng)用程序其余部分的單一來(lái)源。
測(cè)試
我們已經(jīng)提到分離的好處之一就是可測(cè)試性灾杰。讓我們看看我們?nèi)绾螠y(cè)試每個(gè)代碼模塊蚊丐。
用戶(hù)界面和交互:這將是唯一一次你需要一個(gè)Android用戶(hù)界面工具測(cè)試。測(cè)試UI代碼的最好方法是創(chuàng)建一個(gè)Espresso測(cè)試艳吠。您可以創(chuàng)建片段并為其提供一個(gè)模擬的ViewModel麦备。由于該片段只與ViewModel交談,所以嘲笑它將足以完全測(cè)試這個(gè)UI昭娩。
ViewModel:可以使用JUnit測(cè)試來(lái)測(cè)試ViewModel凛篙。你只需要嘲笑UserRepository測(cè)試它。
UserRepository:您也可以UserRepository使用JUnit測(cè)試來(lái)測(cè)試栏渺。你需要嘲笑Webservice和DAO呛梆。您可以測(cè)試它是否進(jìn)行正確的Web服務(wù)調(diào)用,將結(jié)果保存到數(shù)據(jù)庫(kù)中磕诊,如果數(shù)據(jù)已緩存且最新填物,則不會(huì)發(fā)出任何不必要的請(qǐng)求纹腌。由于這兩個(gè)Webservice和UserDao的界面,你可以嘲笑他們或創(chuàng)建更復(fù)雜的測(cè)試案例假冒實(shí)現(xiàn)..
UserDao:測(cè)試DAO類(lèi)的推薦方法是使用儀器測(cè)試滞磺。由于這些儀器測(cè)試不需要任何用戶(hù)界面壶笼,他們?nèi)匀粫?huì)運(yùn)行得很快。對(duì)于每個(gè)測(cè)試雁刷,您可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù)覆劈,以確保測(cè)試沒(méi)有任何副作用(如更改磁盤(pán)上的數(shù)據(jù)庫(kù)文件)。
Room也允許指定數(shù)據(jù)庫(kù)的實(shí)現(xiàn)沛励,所以你可以通過(guò)提供它的JUnit實(shí)現(xiàn)來(lái)測(cè)試它SupportSQLiteOpenHelper责语。通常不建議使用這種方法,因?yàn)樵O(shè)備上運(yùn)行的SQLite版本可能與主機(jī)上的SQLite版本不同目派。
Webservice:使測(cè)試獨(dú)立于外部是很重要的坤候,所以即使你的Webservice測(cè)試也應(yīng)該避免對(duì)后端進(jìn)行網(wǎng)絡(luò)調(diào)用。有很多圖書(shū)館可以幫助你企蹭。例如白筹,MockWebServer是一個(gè)偉大的庫(kù),可以幫助您為測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)器谅摄。
測(cè)試工件體系結(jié)構(gòu)組件提供了一個(gè)Maven工件來(lái)控制其后臺(tái)線程徒河。在android.arch.core:core-testing神器內(nèi)部,有2個(gè)JUnit規(guī)則:
InstantTaskExecutorRule:此規(guī)則可用于強(qiáng)制架構(gòu)組件立即在調(diào)用線程上執(zhí)行任何后臺(tái)操作送漠。
CountingTaskExecutorRule:此規(guī)則可用于檢測(cè)測(cè)試顽照,以等待體系結(jié)構(gòu)組件的后臺(tái)操作或?qū)⑵渥鳛殚e置資源連接到Espresso。
最終的體系結(jié)構(gòu)
下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>
指導(dǎo)原則
編程是一個(gè)創(chuàng)造性的領(lǐng)域闽寡,構(gòu)建Android應(yīng)用程序不是一個(gè)例外代兵。解決問(wèn)題的方法有很多種,可以在多個(gè)活動(dòng)或片段之間傳遞數(shù)據(jù)爷狈,檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進(jìn)行脫機(jī)模式植影,也可以使用許多其他常見(jiàn)應(yīng)用程序遇到的情況。
雖然以下建議不是強(qiáng)制性的涎永,但是我們的經(jīng)驗(yàn)是思币,遵循這些建議將使您的代碼基礎(chǔ)更加健壯,可測(cè)試和可維護(hù)土辩。
您在清單中定義的入口點(diǎn)(活動(dòng)支救,服務(wù),廣播接收器等)不是數(shù)據(jù)的來(lái)源拷淘。相反,他們只應(yīng)該協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集指孤。由于每個(gè)應(yīng)用程序組件的壽命相當(dāng)短启涯,這取決于用戶(hù)與設(shè)備的交互以及運(yùn)行時(shí)的整體當(dāng)前運(yùn)行狀況贬堵,因此您不希望這些入口點(diǎn)中的任何一個(gè)成為數(shù)據(jù)源。
無(wú)情地在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確界定的責(zé)任结洼。例如黎做,不要將從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼跨代碼庫(kù)中的多個(gè)類(lèi)或包傳播。同樣松忍,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個(gè)類(lèi)中蒸殿。
盡可能少地從每個(gè)模塊公開(kāi)。不要試圖創(chuàng)建“只有那一個(gè)”的快捷方式鸣峭,從一個(gè)模塊公開(kāi)內(nèi)部實(shí)現(xiàn)細(xì)節(jié)宏所。您可能在短期內(nèi)獲得一些時(shí)間,但隨著您的代碼庫(kù)的發(fā)展摊溶,您將多次支付技術(shù)債務(wù)爬骤。
在定義模塊之間的交互時(shí),請(qǐng)考慮如何使每個(gè)模塊獨(dú)立地進(jìn)行測(cè)試莫换。例如霞玄,如果有一個(gè)定義良好的API從網(wǎng)絡(luò)中獲取數(shù)據(jù),將會(huì)更容易測(cè)試將數(shù)據(jù)保存在本地?cái)?shù)據(jù)庫(kù)中的模塊拉岁。相反坷剧,如果將這兩個(gè)模塊的邏輯混合在一起,或者在整個(gè)代碼庫(kù)中撒上網(wǎng)絡(luò)代碼喊暖,那么要測(cè)試就更加困難了听隐。
你的應(yīng)用程序的核心是什么讓它從其他中脫穎而出。不要花費(fèi)時(shí)間重復(fù)發(fā)明輪子哄啄,或者一次又一次地寫(xiě)出相同的樣板代碼雅任。相反,將精力集中在讓您的應(yīng)用獨(dú)特的東西上咨跌,讓Android Architecture組件和其他推薦的庫(kù)處理重復(fù)的樣板沪么。
堅(jiān)持盡可能多的相關(guān)和新鮮的數(shù)據(jù),以便您的應(yīng)用程序在設(shè)備處于離線模式時(shí)可用锌半。雖然您可以享受持續(xù)高速的連接禽车,但用戶(hù)可能不會(huì)。
您的存儲(chǔ)庫(kù)應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一的事實(shí)來(lái)源刊殉。無(wú)論何時(shí)您的應(yīng)用程序需要訪問(wèn)這些數(shù)據(jù)殉摔,都應(yīng)該始終從單一的事實(shí)源頭開(kāi)始。有關(guān)更多信息记焊,請(qǐng)參閱單一來(lái)源的真相逸月。
附錄:揭露網(wǎng)絡(luò)狀態(tài)
在上面推薦的應(yīng)用程序體系結(jié)構(gòu)部分,我們故意省略網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)遍膜,以保持樣本簡(jiǎn)單碗硬。在本節(jié)中瓤湘,我們演示一種使用Resource類(lèi)來(lái)公開(kāi)網(wǎng)絡(luò)狀態(tài)的方法來(lái)封裝數(shù)據(jù)及其狀態(tài)。
以下是一個(gè)示例實(shí)現(xiàn):
//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);}}
因?yàn)樵趶拇疟P(pán)顯示數(shù)據(jù)時(shí)從網(wǎng)絡(luò)加載數(shù)據(jù)是一個(gè)常見(jiàn)的用例恩尾,我們將創(chuàng)建一個(gè)NetworkBoundResource可以在多個(gè)地方重復(fù)使用的幫助類(lèi)弛说。以下是決策樹(shù)NetworkBoundResource:
它通過(guò)觀察資源的數(shù)據(jù)庫(kù)開(kāi)始。當(dāng)條目從數(shù)據(jù)庫(kù)中第一次加載時(shí)翰意,NetworkBoundResource檢查結(jié)果是否足夠好以便分派和/或從網(wǎng)絡(luò)中獲取木人。請(qǐng)注意,這兩種情況可能同時(shí)發(fā)生冀偶,因?yàn)槟赡芟M趶木W(wǎng)絡(luò)更新緩存數(shù)據(jù)時(shí)顯示緩存的數(shù)據(jù)醒第。
如果網(wǎng)絡(luò)呼叫成功完成,則將響應(yīng)保存到數(shù)據(jù)庫(kù)中并重新初始化流蔫磨。如果網(wǎng)絡(luò)請(qǐng)求失敗淘讥,我們直接發(fā)送失敗。
注意:在將新數(shù)據(jù)保存到磁盤(pán)之后堤如,我們會(huì)重新初始化數(shù)據(jù)庫(kù)中的數(shù)據(jù)流蒲列,但通常我們不需要這樣做,因?yàn)閿?shù)據(jù)庫(kù)將分派更改搀罢。另一方面蝗岖,依靠數(shù)據(jù)庫(kù)來(lái)調(diào)度變化將依賴(lài)于不好的副作用,因?yàn)槿绻麛?shù)據(jù)沒(méi)有變化榔至,數(shù)據(jù)庫(kù)可以避免調(diào)度變化抵赢,那么它可能會(huì)中斷。我們也不希望發(fā)送從網(wǎng)絡(luò)到達(dá)的結(jié)果唧取,因?yàn)檫@將違背單一的事實(shí)來(lái)源(也許在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變保存的值)铅鲤。我們也不想SUCCESS沒(méi)有新的數(shù)據(jù),因?yàn)樗鼤?huì)向客戶(hù)發(fā)送錯(cuò)誤的信息枫弟。
以下是NetworkBoundResource班級(jí)為其子女提供的公共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();}
請(qǐng)注意邢享,上面的類(lèi)定義了兩個(gè)類(lèi)型參數(shù)(ResultType,RequestType)淡诗,因?yàn)閺腁PI返回的數(shù)據(jù)類(lèi)型可能與本地使用的數(shù)據(jù)類(lèi)型不匹配骇塘。
另請(qǐng)注意,上面的代碼ApiResponse用于網(wǎng)絡(luò)請(qǐng)求韩容。ApiResponse是一個(gè)簡(jiǎn)單的Retrofit2.Call類(lèi)包裝款违,將其響應(yīng)轉(zhuǎn)換為L(zhǎng)iveData。
以下是該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;}}
現(xiàn)在群凶,我們可以使用它NetworkBoundResource來(lái)User在存儲(chǔ)庫(kù)中寫(xiě)入我們的磁盤(pán)和網(wǎng)絡(luò)綁定實(shí)現(xiàn)插爹。
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();}}