Android官方架構(gòu)組件指南

此指南適用于那些曾經(jīng)或現(xiàn)在進(jìn)行Android應(yīng)用的基礎(chǔ)開(kāi)發(fā),并希望了解和學(xué)習(xí)編寫(xiě)Android程序的最佳實(shí)踐和架構(gòu)追城。通過(guò)學(xué)習(xí)來(lái)構(gòu)建強(qiáng)大的生產(chǎn)級(jí)別的應(yīng)用刹碾。

注意:此指南默認(rèn)你對(duì)Android開(kāi)發(fā)有比較深的理解,熟知Android Framework座柱。如果你還只是個(gè)Android開(kāi)發(fā)新手教硫,那么建議先學(xué)習(xí)下Android的基礎(chǔ)知識(shí)。

Android程序員面臨的問(wèn)題

傳統(tǒng)的桌面應(yīng)用程序開(kāi)發(fā)在大多數(shù)情況下辆布,啟動(dòng)器快捷方式都有一個(gè)入口點(diǎn)瞬矩,并作為一個(gè)單一的過(guò)程運(yùn)行,但Android應(yīng)用程序的結(jié)構(gòu)更為復(fù)雜锋玲。典型的Android應(yīng)用程序由多個(gè)應(yīng)用程序組件構(gòu)成景用,包括Activity,F(xiàn)ragment惭蹂,Service伞插,ContentProvider和Broadcast Receiver。

大多數(shù)這些應(yīng)用程序組件在Android操作系統(tǒng)使用的AndroidManifest中聲明盾碗,以決定如何將應(yīng)用程序集成到設(shè)備上來(lái)為用戶(hù)提供完整的體驗(yàn)媚污。盡管如前所述,桌面應(yīng)用程序傳統(tǒng)上是作為一個(gè)單一的進(jìn)程運(yùn)行的廷雅,但正確編寫(xiě)的Android應(yīng)用程序則需要更靈活耗美,因?yàn)橛脩?hù)通過(guò)設(shè)備上的不同應(yīng)用程序編織方式,不斷切換流程和任務(wù)航缀。

舉個(gè)例子商架,當(dāng)用戶(hù)在社交App上打算分享一張照片,那么Android系統(tǒng)就會(huì)為此啟動(dòng)相機(jī)來(lái)完成此次請(qǐng)求芥玉。此時(shí)用戶(hù)離開(kāi)了社交App蛇摸,但是這個(gè)用戶(hù)體驗(yàn)是無(wú)縫連接的。相機(jī)可能又會(huì)觸發(fā)并啟動(dòng)文件管理器來(lái)選擇照片灿巧。最終回到社交App并分享照片赶袄。此外,在此過(guò)程中的任何時(shí)候抠藕,用戶(hù)可能會(huì)被打電話(huà)中斷饿肺,并在完成電話(huà)后再回來(lái)分享照片。

在Android中幢痘,這種應(yīng)用間跳轉(zhuǎn)行為很常見(jiàn)唬格,因此你的應(yīng)用必須正確處理這些流程家破。請(qǐng)記住颜说,移動(dòng)設(shè)備是資源有限的购岗,所以在任何時(shí)候,操作系統(tǒng)可能需要?dú)⑺酪恍?yīng)用來(lái)為新的應(yīng)用騰出空間门粪。

你的應(yīng)用程序的所有組件都可以被單獨(dú)啟動(dòng)或無(wú)序啟動(dòng)喊积,并且在任何時(shí)候由用戶(hù)或系統(tǒng)銷(xiāo)毀。因?yàn)閼?yīng)用程序組件是短暫的玄妈,它們的生命周期(創(chuàng)建和銷(xiāo)毀時(shí))不受你的控制乾吻,因此你不應(yīng)該將任何應(yīng)用程序數(shù)據(jù)或狀態(tài)存儲(chǔ)在應(yīng)用程序組件中,并且應(yīng)用程序組件不應(yīng)相互依賴(lài)拟蜻。

常見(jiàn)的架構(gòu)原理

如果你無(wú)法使用應(yīng)用程序組件來(lái)存儲(chǔ)應(yīng)用程序數(shù)據(jù)和狀態(tài)绎签,應(yīng)如何構(gòu)建應(yīng)用程序?

在你的App開(kāi)發(fā)中你應(yīng)該將重心放在分層上酝锅,如果將所有的代碼都寫(xiě)在Activity或者Fragment中诡必,那問(wèn)題就大了。任何不是處理UI或跟操作系統(tǒng)交互的操作不應(yīng)該放在這兩個(gè)類(lèi)中搔扁。盡量保持它們代碼的精簡(jiǎn)爸舒,這樣你可以避免很多與生命周期相關(guān)的問(wèn)題。記住你并不能掌控Activity和Fragment稿蹲,他們只是在你的App和Android系統(tǒng)間起了橋梁的作用扭勉。任何時(shí)候,Android系統(tǒng)可能會(huì)根據(jù)用戶(hù)操作或其他因素(如低內(nèi)存)來(lái)回收它們苛聘。最好盡量減少對(duì)他們的依賴(lài)涂炎,以提供堅(jiān)實(shí)的用戶(hù)體驗(yàn)。

還有一點(diǎn)比較重要的就是持久模型驅(qū)動(dòng)UI设哗。使用持久模型主要是因?yàn)楫?dāng)你的UI被回收或者在沒(méi)有網(wǎng)絡(luò)的情況下還能正常給用戶(hù)展示數(shù)據(jù)璧尸。模型是用來(lái)處理應(yīng)用數(shù)據(jù)的組件,它們獨(dú)立于應(yīng)用中的視圖和四大組件熬拒。因此模型的生命周期必然和UI是分離的爷光。保持UI代碼的整潔,會(huì)讓你能更容易的管理和調(diào)整UI澎粟。讓你的應(yīng)用基于模型開(kāi)發(fā)可以很好的管理你應(yīng)用的數(shù)據(jù)并是你的應(yīng)用更具測(cè)試性和持續(xù)性蛀序。

應(yīng)用架構(gòu)推薦

回到這篇文章的主題,來(lái)說(shuō)說(shuō)Android官方架構(gòu)組件(一下簡(jiǎn)稱(chēng)架構(gòu))活烙。一下會(huì)介紹如何在你的應(yīng)用中實(shí)踐這一架構(gòu)模式徐裸。

注意:不可能存在某一種架構(gòu)方式可以完美適合任何場(chǎng)景。話(huà)雖如此啸盏,這種架構(gòu)應(yīng)該是大多數(shù)用例的良好起點(diǎn)重贺。如果你已經(jīng)有了很好的Android應(yīng)用程序架構(gòu)方式,請(qǐng)繼續(xù)保持。

假設(shè)我們需要一個(gè)現(xiàn)實(shí)用戶(hù)資料的UI气笙,該用戶(hù)的資料文件將使用REST API從服務(wù)端獲取次企。

構(gòu)建用戶(hù)界面

我們的這個(gè)用戶(hù)界面由一個(gè)UserProfileFragment.java文件和它的布局文件user_profile_layout.xml。

為了驅(qū)動(dòng)UI潜圃,數(shù)據(jù)模型需要持有下面兩個(gè)數(shù)據(jù):

User ID:用戶(hù)的標(biāo)識(shí)符缸棵。最好使用Fragment的參數(shù)將此信息傳遞到Fragment中。如果Android操作系統(tǒng)回收了Fragment谭期,則會(huì)保留此信息堵第,以便下次重新啟動(dòng)應(yīng)用時(shí),該ID可用隧出。

User Object:傳統(tǒng)的Java對(duì)象踏志,代表用戶(hù)的數(shù)據(jù)。

為此胀瞪,我們新建一個(gè)繼承自ViewModel的名為UserProfileViewModel的模型來(lái)持有這個(gè)數(shù)據(jù)狰贯。

ViewModel提供特定UI組件的數(shù)據(jù),例如Activity和Fragment赏廓,并處理與數(shù)據(jù)處理業(yè)務(wù)部分的通信涵紊,例如調(diào)用其他組件來(lái)加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶(hù)修改。ViewModel不了解View幔摸,并且不受UI的重建(如重由于旋轉(zhuǎn)而導(dǎo)致的Activity的重建)的影響摸柄。

現(xiàn)在我們有一下三個(gè)文件:

user_profile.xml: 視圖的布局文件。

UserProfileViewModel.java: 持有UI數(shù)據(jù)的模型既忆。

UserProfileFragment.java: 用于顯示數(shù)據(jù)模型中的數(shù)據(jù)并和用戶(hù)進(jìn)行交互驱负。

一下是具體代碼(為了簡(jiǎn)化,布局文件省略)患雇。

注意:上面的UserProfileFragment繼承自L(fǎng)ifeCycleFragment而不是Fragment跃脊。當(dāng)Lifecycle的Api穩(wěn)定后,F(xiàn)ragment會(huì)默認(rèn)實(shí)現(xiàn)LifeCycleOwner苛吱。

現(xiàn)在酪术,我們有三個(gè)文件,我們?nèi)绾芜B接它們翠储?畢竟绘雁,當(dāng)ViewModel的用戶(hù)字段被設(shè)置時(shí),我們需要一種通知UI的方法援所。這里就要提到LiveData了庐舟。

LiveData是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData對(duì)象持有的數(shù)據(jù)住拭,而不會(huì)在它們之間創(chuàng)建顯式和剛性的依賴(lài)路徑挪略。LiveData還尊重你的應(yīng)用程序組件(Activity历帚,F(xiàn)ragment,Service)的生命周期狀態(tài)杠娱,并做正確的事情以防止內(nèi)存泄漏挽牢,從而你的應(yīng)用程序不會(huì)消耗更多的內(nèi)存。

如果你已經(jīng)使用了想Rxjava活著Agrea這類(lèi)第三方庫(kù)墨辛,那么你可以使用它們代替LiveData,不過(guò)你需要處理好它們與組件生命周期之間的關(guān)系趴俘。

現(xiàn)在我們使用LiveData來(lái)代替UserProfileViewModel中的User字段睹簇。所以Fragment可以通過(guò)觀察它來(lái)更新數(shù)據(jù)。LiveData值得稱(chēng)道的地方就在于它是生命周期感知的寥闪,當(dāng)生命周期結(jié)束是太惠,其上的觀察者會(huì)被即使清理。

然后將UserProfileFragment修改如下疲憋,觀察數(shù)據(jù)并更新UI:

一旦用戶(hù)數(shù)據(jù)更新凿渊,onChanged回調(diào)將被調(diào)用然后UI會(huì)被刷新。

如果你熟悉一些使用觀察者模式第三方庫(kù)缚柳,你會(huì)覺(jué)得奇怪埃脏,為什么沒(méi)有在Fragment的onStop()方法中將觀察者移除。對(duì)于LiveData來(lái)說(shuō)這是沒(méi)有必要的秋忙,因?yàn)樗巧芷诟兄牟势@意味著如果UI處于不活動(dòng)狀態(tài),它就不會(huì)調(diào)用觀察者的回調(diào)來(lái)更新數(shù)據(jù)灰追。并且在onDestroy后會(huì)自動(dòng)移除堵幽。

我們也不需要處理任何視圖重建(如屏幕旋轉(zhuǎn))。ViewModel會(huì)自動(dòng)恢復(fù)重建前的數(shù)據(jù)弹澎。當(dāng)新的視圖被創(chuàng)建出來(lái)后朴下,它會(huì)接收到與之前相同的ViewModel實(shí)例,并且觀察者的回調(diào)會(huì)被立刻調(diào)用苦蒿,更新最新的數(shù)據(jù)殴胧。這也是ViewModel為什么不能直接引用視圖對(duì)象,因?yàn)樗纳芷陂L(zhǎng)于視圖對(duì)象佩迟。

獲取數(shù)據(jù)

現(xiàn)在我們將視圖和模型連接起來(lái)溃肪,但是模型該怎么獲取數(shù)據(jù)呢?在這個(gè)例子中音五,我們假設(shè)使用REST API從后臺(tái)獲取惫撰。我們將使用Retrofit來(lái)向后臺(tái)請(qǐng)求數(shù)據(jù)。

我們的retrofit類(lèi)Webservice如下:

如果只是簡(jiǎn)單的實(shí)現(xiàn)躺涝,ViewModel可以直接操作Webservice來(lái)獲取用戶(hù)數(shù)據(jù)厨钻。雖然這樣可以正常工作扼雏,但你的應(yīng)用無(wú)法保證它的后續(xù)迭代。因?yàn)檫@樣做將太多的責(zé)任讓ViewModel來(lái)承擔(dān)夯膀,這樣就違反類(lèi)之前講到的分層原則诗充。又因?yàn)閂iewModel的生命周期是綁定在Activity和Fragment上的,所以當(dāng)UI被銷(xiāo)毀后如果丟失所有數(shù)據(jù)將是很差的用戶(hù)體驗(yàn)诱建。所以我們的ViewModel將和一個(gè)新的模塊進(jìn)行交互蝴蜓,這個(gè)模塊叫Repository。

Repository模塊負(fù)責(zé)處理數(shù)據(jù)俺猿。它為應(yīng)用程序的其余部分提供了一個(gè)干凈的API茎匠。他知道在數(shù)據(jù)更新時(shí)從哪里獲取數(shù)據(jù)和調(diào)用哪些API調(diào)用。你可以將它們視為不同數(shù)據(jù)源(持久性模型押袍,Web服務(wù)诵冒,緩存等)之間的中介者。

UserRepository類(lèi)如下:

雖然repository模塊看上去沒(méi)有必要谊惭,但他起著重要的作用汽馋。它為App的其他部分抽象出了數(shù)據(jù)源∪現(xiàn)在我們的ViewModel并不知道數(shù)據(jù)是通過(guò)WebService來(lái)獲取的豹芯,這意味著我們可以隨意替換掉獲取數(shù)據(jù)的實(shí)現(xiàn)。

管理組件間的依賴(lài)關(guān)系

上面這種寫(xiě)法可以看出來(lái)UserRepository需要初始化Webservice實(shí)例,這雖然說(shuō)起來(lái)簡(jiǎn)單,但要實(shí)現(xiàn)的話(huà)還需要知道Webservice的具體構(gòu)造方法該如何寫(xiě)。這將加大代碼的復(fù)雜度矫俺,另外UserRepository可能并不是唯一使用Webservice的對(duì)象铅匹,所以這種在內(nèi)部構(gòu)建Webservice實(shí)例顯然是不推薦的,下面有兩種模式來(lái)解決這個(gè)問(wèn)題:

依賴(lài)注入:依賴(lài)注入允許類(lèi)定義它們的依賴(lài)關(guān)系而不構(gòu)造它們萌抵。在運(yùn)行時(shí)哆档,另一個(gè)類(lèi)負(fù)責(zé)提供這些依賴(lài)關(guān)系澳淑。我們建議在Android應(yīng)用程序中使用Google的Dagger 2庫(kù)實(shí)現(xiàn)依賴(lài)注入叁怪。Dagger 2通過(guò)遍歷依賴(lài)關(guān)系樹(shù)自動(dòng)構(gòu)建對(duì)象奕谭,并在依賴(lài)關(guān)系上提供編譯時(shí)保證昆汹。

服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊(cè)表,其中類(lèi)可以獲取它們的依賴(lài)關(guān)系而不是構(gòu)造它們。與依賴(lài)注入(DI)相比,實(shí)現(xiàn)起來(lái)相對(duì)容易,因此如果您不熟悉DI,請(qǐng)改用Service Locator内边。

這些模式允許你擴(kuò)展代碼榴都,因?yàn)樗鼈兲峁┟鞔_的模式來(lái)管理依賴(lài)關(guān)系,而不會(huì)重復(fù)代碼或增加復(fù)雜性假残。兩者都允許交換實(shí)現(xiàn)進(jìn)行測(cè)試;這是使用它們的主要好處之一缭贡。在這個(gè)例子中,我們將使用Dagger 2來(lái)管理依賴(lài)關(guān)系辉懒。

連接ViewModel和Repository

現(xiàn)在阳惹,我們的UserProfileViewModel可以改寫(xiě)成這樣:

緩存數(shù)據(jù)

上面的Repository雖然網(wǎng)絡(luò)請(qǐng)求做了封裝,但是它依賴(lài)后臺(tái)數(shù)據(jù)源眶俩,所以存在不足莹汤。

上面的UserRepository實(shí)現(xiàn)的問(wèn)題是,在獲取數(shù)據(jù)之后颠印,它不會(huì)保留在任何地方纲岭。如果用戶(hù)離開(kāi)UserProfileFragment并重新進(jìn)來(lái)抹竹,則應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)。這是不好的止潮,有兩個(gè)原因:它浪費(fèi)了寶貴的網(wǎng)絡(luò)帶寬和迫使用戶(hù)等待新的查詢(xún)完成窃判。為了解決這個(gè)問(wèn)題,我們將向我們的UserRepository添加一個(gè)新的數(shù)據(jù)源喇闸,它將把User對(duì)象緩存在內(nèi)存中袄琳。如下:

持久化數(shù)據(jù)

在當(dāng)前的實(shí)現(xiàn)中,如果用戶(hù)旋轉(zhuǎn)屏幕或離開(kāi)并返回到應(yīng)用程序,現(xiàn)有UI將立即可見(jiàn),因?yàn)镽epository會(huì)從內(nèi)存中檢索數(shù)據(jù)勾邦。但是,如果用戶(hù)離開(kāi)應(yīng)用程序逗旁,并在Android操作系統(tǒng)殺死進(jìn)程后幾小時(shí)后又會(huì)怎么樣?

在目前的實(shí)現(xiàn)中舆瘪,我們將需要從網(wǎng)絡(luò)中再次獲取數(shù)據(jù)片效。這不僅是一個(gè)糟糕的用戶(hù)體驗(yàn),也是浪費(fèi)介陶,因?yàn)樗鼘⑹褂靡苿?dòng)數(shù)據(jù)來(lái)重新獲取相同的數(shù)據(jù)堤舒。你以通過(guò)緩存Web請(qǐng)求來(lái)簡(jiǎn)單地解決這個(gè)問(wèn)題色建,但它會(huì)產(chǎn)生新的問(wèn)題哺呜。如果請(qǐng)求一個(gè)朋友列表而不是單個(gè)用戶(hù),會(huì)發(fā)生什么情況箕戳?那么你的應(yīng)用程序可能會(huì)顯示不一致的數(shù)據(jù)某残,這是最令人困惑的用戶(hù)體驗(yàn)。例如陵吸,相同的用戶(hù)的數(shù)據(jù)可能會(huì)不同玻墅,因?yàn)榕笥蚜斜碚?qǐng)求和用戶(hù)請(qǐng)求可以在不同的時(shí)間執(zhí)行。你的應(yīng)用需要合并他們壮虫,以避免顯示不一致的數(shù)據(jù)澳厢。

正確的處理方法是使用持久模型。這時(shí)候Room就派上用場(chǎng)了囚似。

Room是一個(gè)對(duì)象映射庫(kù)剩拢,它提供本地?cái)?shù)據(jù)持久性和最少的樣板代碼。在編譯時(shí)饶唤,它根據(jù)模式驗(yàn)證每個(gè)查詢(xún)徐伐,從而錯(cuò)誤的SQL查詢(xún)會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤,而不是運(yùn)行時(shí)失敗募狂。Room抽象了使用原始SQL表和查詢(xún)的一些基本實(shí)現(xiàn)細(xì)節(jié)办素。它還允許觀察數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢(xún))的更改角雷,通過(guò)LiveData對(duì)象公開(kāi)這些更改。

要使用Room我們首先需要使用@Entity來(lái)定義實(shí)體:

接著創(chuàng)建數(shù)據(jù)庫(kù)類(lèi):

值得注意的是MyDatabase是一個(gè)抽象了性穿,Room會(huì)在編譯期間提供它的一個(gè)實(shí)現(xiàn)類(lèi)勺三。

接下來(lái)需要定義DAO:

接著在MyDatabase中添加獲取上面這個(gè)DAO的方法:

這里的load方法返回的是LiveData

現(xiàn)在我們可以修改UserRepository了:

這里雖然我們將UserRepository的直接數(shù)據(jù)來(lái)源從Webservice改為本地?cái)?shù)據(jù)庫(kù),但我們卻不需要修改UserProfileViewModel或者UserProfileFragment需曾。這就是抽象層帶來(lái)的好處檩咱。這也給測(cè)試帶來(lái)了方便,因?yàn)槟憧梢蕴峁┮粋€(gè)虛假的UserRepository來(lái)測(cè)試你的UserProfileViewModel胯舷。

現(xiàn)在刻蚯,如果用戶(hù)重新回到這個(gè)界面,他們會(huì)立刻看到數(shù)據(jù)桑嘶,因?yàn)槲覀円呀?jīng)將數(shù)據(jù)做了持久化的保存炊汹。當(dāng)然如果有用例需要,我們也可不展示太老舊的持久化數(shù)據(jù)逃顶。

在一些用例中讨便,比如下拉刷新,如果正處于網(wǎng)絡(luò)請(qǐng)求中以政,那UI需要告訴用戶(hù)正處于網(wǎng)絡(luò)請(qǐng)求中霸褒。一個(gè)好的實(shí)踐方式就是將UI與數(shù)據(jù)分離,因?yàn)閁I可能因?yàn)楦鞣N原因被更新盈蛮。從UI的角度來(lái)說(shuō)废菱,請(qǐng)求中的數(shù)據(jù)和本地?cái)?shù)據(jù)類(lèi)似,只是它還沒(méi)有被持久化到數(shù)據(jù)庫(kù)中抖誉。

以下有兩種解決方法:

將getUser的返回值中加入網(wǎng)絡(luò)狀態(tài)殊轴。

在Repository中提供一個(gè)可以返回刷新?tīng)顟B(tài)的方法。如果你只是想在用戶(hù)通過(guò)下拉刷新來(lái)告訴用戶(hù)目前的網(wǎng)絡(luò)狀態(tài)的話(huà)袒炉,那這個(gè)方法是比較適合的旁理。

數(shù)據(jù)唯一來(lái)源

在以上實(shí)例中,數(shù)據(jù)唯一來(lái)源是數(shù)據(jù)庫(kù)我磁,這樣做的好處是用戶(hù)可以基于穩(wěn)定的數(shù)據(jù)庫(kù)數(shù)據(jù)來(lái)更新頁(yè)面孽文,而不需要處理大量的網(wǎng)絡(luò)請(qǐng)求狀態(tài)。數(shù)據(jù)庫(kù)有數(shù)據(jù)則使用夺艰,沒(méi)有數(shù)據(jù)則等待其更新芋哭。

測(cè)試

我們之前提到分層可以個(gè)應(yīng)用提供良好的測(cè)試能力,接下來(lái)就看看我們?cè)趺礈y(cè)試不同的模塊劲适。

用戶(hù)界面與交互:這是唯一一個(gè)需要使用到Android UI Instrumentation test的測(cè)試模塊楷掉。測(cè)試UI的最好方法就是使用Espresso框架。你可以創(chuàng)建Fragment然后提供一個(gè)虛假的ViewModel。因?yàn)镕ragment只跟ViewModel交互烹植,所以虛擬一個(gè)ViewModel就足夠了斑鸦。

ViewModel:ViewModel可以用JUnit test進(jìn)行測(cè)試。因?yàn)槠洳簧婕敖缑媾c交互草雕。而且你只需要虛擬UserRepository即可巷屿。

UserRepository:測(cè)試UserRepository同樣使用JUnit test。你可以虛擬出Webservice和DAO墩虹。你可以通過(guò)使用正確的網(wǎng)絡(luò)請(qǐng)求來(lái)請(qǐng)求數(shù)據(jù)嘱巾,讓后將數(shù)據(jù)通過(guò)DAO寫(xiě)入數(shù)據(jù)庫(kù)。如果數(shù)據(jù)庫(kù)中有相關(guān)數(shù)據(jù)則無(wú)需進(jìn)行網(wǎng)絡(luò)請(qǐng)求诫钓。

UserDao:對(duì)于DAO的測(cè)試旬昭,推薦使用instrumentation進(jìn)行測(cè)試。因?yàn)榇颂師o(wú)需UI菌湃,并且可以使用in-memory數(shù)據(jù)庫(kù)來(lái)保證測(cè)試的封閉性问拘,不會(huì)影響到磁盤(pán)上的數(shù)據(jù)庫(kù)。

Webservice:保持測(cè)試的封閉性是相當(dāng)重要的惧所,因此即使是你的Webservice測(cè)試也應(yīng)避免對(duì)后端進(jìn)行網(wǎng)絡(luò)呼叫骤坐。有很多第三方庫(kù)提供這方面的支持。例如下愈,MockWebServer是一個(gè)很棒的庫(kù)纽绍,可以幫助你為你的測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)器。

架構(gòu)圖

指導(dǎo)原則

編程是一個(gè)創(chuàng)意領(lǐng)域势似,構(gòu)建Android應(yīng)用程序也不例外拌夏。有多種方法來(lái)解決問(wèn)題,無(wú)論是在多個(gè)Activity或Fragment之間傳遞數(shù)據(jù)叫编,還是檢索遠(yuǎn)程數(shù)據(jù)并將其在本地保持離線(xiàn)模式辖佣,或者是任何其他常見(jiàn)的場(chǎng)景霹抛。

雖然以下建議不是強(qiáng)制性的搓逾,但經(jīng)驗(yàn)告訴我們,遵循這些建議將使你的代碼庫(kù)從長(zhǎng)遠(yuǎn)來(lái)看更加強(qiáng)大杯拐,可測(cè)試和可維護(hù)霞篡。

在AndroidManifest中定義的Activity,Service端逼,Broadcast Receiver等朗兵,它們不是數(shù)據(jù)源。相反顶滩,他們只是用于協(xié)調(diào)和展示數(shù)據(jù)余掖。由于每個(gè)應(yīng)用程序組件的壽命相當(dāng)短,運(yùn)行狀態(tài)取決于用戶(hù)與其設(shè)備的交互以及運(yùn)行時(shí)的整體當(dāng)前運(yùn)行狀況礁鲁,所以不要將這些組件作為數(shù)據(jù)源盐欺。

你需要在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確界定的責(zé)任范圍赁豆。例如,不要在不同的類(lèi)或包之間傳遞用于加載網(wǎng)絡(luò)數(shù)據(jù)的代碼冗美。同樣魔种,不要將數(shù)據(jù)緩存和數(shù)據(jù)綁定這兩個(gè)責(zé)任完全不同的放在同一個(gè)類(lèi)中。

每個(gè)模塊之間要竟可能少的相互暴露粉洼。不要抱有僥幸心理去公開(kāi)一個(gè)關(guān)于模塊的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的接口节预。你可能會(huì)在短期內(nèi)獲得到便捷,但是隨著代碼庫(kù)的發(fā)展属韧,你將多付多次技術(shù)性債務(wù)安拟。

當(dāng)你定義模塊之間的交互時(shí),請(qǐng)考慮如何使每個(gè)模塊隔離宵喂。例如去扣,擁有用于從網(wǎng)絡(luò)中提取數(shù)據(jù)的定義良好的API將使得更容易測(cè)試在本地?cái)?shù)據(jù)庫(kù)中持久存在該數(shù)據(jù)的模塊。相反樊破,如果將這兩個(gè)模塊的邏輯組合在一起愉棱,或者將整個(gè)代碼庫(kù)中的網(wǎng)絡(luò)代碼放在一起,那么測(cè)試就更難(如果不是不可能)哲戚。

你的應(yīng)用程序的核心是什么讓它獨(dú)立出來(lái)奔滑。不要花時(shí)間重復(fù)輪子或一次又一次地編寫(xiě)相同的樣板代碼。相反顺少,將精力集中在使你的應(yīng)用程序獨(dú)一無(wú)二的同時(shí)朋其,讓Android架構(gòu)組件和其他推薦的庫(kù)來(lái)處理重復(fù)的樣板代碼。

保持盡可能多的相關(guān)聯(lián)的新鮮數(shù)據(jù)脆炎,以便你的應(yīng)用程序在設(shè)備處于脫機(jī)模式時(shí)可用梅猿。雖然你可以享受恒定和高速連接,但你的用戶(hù)可能不會(huì)秒裕。

你的Repository應(yīng)指定一個(gè)數(shù)據(jù)源作為真實(shí)的單一來(lái)源袱蚓。每當(dāng)你的應(yīng)用程序需要訪(fǎng)問(wèn)這些數(shù)據(jù)時(shí),它應(yīng)該始終源于真實(shí)的單一來(lái)源几蜻。

擴(kuò)展: 公開(kāi)網(wǎng)絡(luò)狀態(tài)

在上面的小結(jié)我們故意省略了網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)來(lái)保證例子的簡(jiǎn)潔性喇潘。在這一小結(jié)我們演示一種使用Resource類(lèi)來(lái)封裝數(shù)據(jù)及其狀態(tài)。以此來(lái)公開(kāi)網(wǎng)絡(luò)狀態(tài)梭稚。

下面是簡(jiǎn)單的Resource實(shí)現(xiàn):

以為從網(wǎng)絡(luò)上抓取視頻的同時(shí)在UI上顯示數(shù)據(jù)庫(kù)的舊數(shù)據(jù)是很常見(jiàn)的用例颖低,所以我們要?jiǎng)?chuàng)建一個(gè)可以在多個(gè)地方重復(fù)使用的幫助類(lèi)NetworkBoundResource。以下是NetworkBoundResource的決策樹(shù):

NetworkBoundResource從觀察數(shù)據(jù)庫(kù)開(kāi)始弧烤,當(dāng)?shù)谝淮螐臄?shù)據(jù)庫(kù)加載完實(shí)體后忱屑,NetworkBoundResource會(huì)檢查這個(gè)結(jié)果是否滿(mǎn)足用來(lái)展示的需求,如不滿(mǎn)足則需要從網(wǎng)上重新獲取。當(dāng)然以上兩種情況可能同時(shí)發(fā)生莺戒,你希望先將數(shù)據(jù)顯示在UI上的同時(shí)去網(wǎng)絡(luò)上請(qǐng)求新數(shù)據(jù)粱栖。

如果網(wǎng)絡(luò)請(qǐng)求成果,則將結(jié)果保存到數(shù)據(jù)庫(kù)脏毯,然后重新從數(shù)據(jù)庫(kù)加載數(shù)據(jù)闹究,如果網(wǎng)絡(luò)請(qǐng)求失敗,則直接傳遞錯(cuò)誤信息食店。

注意:在上面的過(guò)程中可以看到當(dāng)將新數(shù)據(jù)保存到數(shù)據(jù)庫(kù)后渣淤,我們重新從數(shù)據(jù)庫(kù)加載數(shù)據(jù)。雖然大部分情況我們不必如此吉嫩,因?yàn)閿?shù)據(jù)庫(kù)會(huì)為我們傳遞此次更新价认。但另一方面,依賴(lài)數(shù)據(jù)庫(kù)內(nèi)部的更新機(jī)制并不是我們想要的如果更新的數(shù)據(jù)與舊數(shù)據(jù)一致自娩,則數(shù)據(jù)谷不會(huì)做出更新提示用踩。我們也不希望直接從網(wǎng)絡(luò)請(qǐng)求中獲取數(shù)據(jù)直接用于UI,因?yàn)檫@樣違背了單一數(shù)據(jù)源的原則忙迁。

下面是NetworkBoundResource類(lèi)的公共api:

注意到上面定義了兩種泛型脐彩,ResultType和RequestType,因?yàn)閺木W(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)類(lèi)型可能會(huì)和數(shù)據(jù)庫(kù)返回的不一致姊扔。

另外注意到上面代碼中的ApiResponse這個(gè)類(lèi)惠奸,他是將Retroft2.Call轉(zhuǎn)換成LiveData的一個(gè)簡(jiǎn)單封裝。

下面是NetworkBoundResource余下部分的實(shí)現(xiàn):

接著我們就可以在UserRepository中使用NetworkBoundResource了恰梢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末佛南,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嵌言,更是在濱河造成了極大的恐慌嗅回,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摧茴,死亡現(xiàn)場(chǎng)離奇詭異绵载,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蓬蝶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)尘分,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人丸氛,你說(shuō)我怎么就攤上這事≈ぃ” “怎么了缓窜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我禾锤,道長(zhǎng)私股,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任恩掷,我火速辦了婚禮倡鲸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黄娘。我一直安慰自己峭状,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布逼争。 她就那樣靜靜地躺著优床,像睡著了一般。 火紅的嫁衣襯著肌膚如雪誓焦。 梳的紋絲不亂的頭發(fā)上胆敞,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音杂伟,去河邊找鬼移层。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赫粥,可吹牛的內(nèi)容都是我干的幽钢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼傅是,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼匪燕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起喧笔,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤帽驯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后书闸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體尼变,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年浆劲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫌术。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡牌借,死狀恐怖度气,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膨报,我是刑警寧澤磷籍,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布适荣,位于F島的核電站,受9級(jí)特大地震影響院领,放射性物質(zhì)發(fā)生泄漏弛矛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一比然、第九天 我趴在偏房一處隱蔽的房頂上張望丈氓。 院中可真熱鬧,春花似錦强法、人聲如沸万俗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)该编。三九已至,卻和暖如春硕淑,著一層夾襖步出監(jiān)牢的瞬間课竣,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工置媳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留于樟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓拇囊,卻偏偏與公主長(zhǎng)得像迂曲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寥袭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容