此指南適用于那些曾經(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了恰梢。