這是一篇 Android Architecture Components 的簡單使用指南,目的是向大家介紹這麼一種新的架構方案。Android Architecture Components 是一個由官方推出的新庫,它能夠幫助你去構建一個健壯,易測,可維護的應用。目前它還未正式發布(Now available in preview)。所以抱著強烈的好奇心去了解了一下。
本文譯自 Guide to App Architecture, 並結合自己的理解後記錄下來。鏈接中有更多的細節可以參考。如果有認識錯誤的地方歡迎指出以修正。
Note:
這篇指南適合於已經有 Android 應用開發經驗的工程師。如果你才剛入坑,查看入門文檔可能會更有幫助。
現存問題
移動開發不同於傳統的桌麵 PC 開發, Android 應用的結構更加複雜。一個典型的 Android 應用由多個應用組件組合構建而成,其中包括 Activities,Fragments,Services,ContentProviders和BroadcastReceivers。
大部分這些應用組件被定義在 AndroidManifest.xml 文件中,這個文件被 Android 係統用於決定怎麼去整合構建你的應用程序給用戶帶來一個好的用戶體驗。Android 應用的使用場景非常靈活,用戶往往是為了完成一個動作而在不同的應用之間頻繁的切換跳轉。
試想一下當你想用社交應用分享一張照片時會發生什麼。首先這個社交應用會使用 Android 的拍照接口直接使用係統中的相機應用完成拍照的請求。在這個時候,用戶已經離開了這個社交應用但是這個體驗是完全無縫的。這個相機應用可能又會使用 Android 的其他接口,比如打開一個文件選擇器,這時候會跳轉到係統中的另一個實現了文件選擇功能的應用。最終,用戶又回到了社交應用去分享之前操作中最後選定的一張照片。在這個過程中用戶還可能隨時被一個來電打斷,在通話完成後又繼續分享照片。
沒錯,在 Android 中,應用間的這種跳躍切換行為很普遍,所以你的應用程序必須把這些問題都正確的處理好。要知道移動設備的硬件資源是很有限的,在任何時候係統都可能殺掉一些應用去釋放一些資源給新的應用。
所有的這些都說明你的應用組件的創建和銷毀是不完全可控的,它可能在任何時候由於用戶或者係統的行為而觸發。應用組件的生命周期不是完全由你掌控的,所以你不應該存儲一些數據或者狀態在你的應用組件中,應用組件之間也不應該彼此依賴。
架構原則
如果不能用應用組件去存儲應用數據和狀態,那應該怎樣去設計應用的架構呢?
首先,通常的架構原則有幾個重點:
第一個重點是在應用中的關注點分離。比如在一個 Activity 或者 Fragment 中寫所有的代碼這明顯是錯誤的。任何與 UI 或者交互無關的代碼都不應該存在這些類中。保證他們盡可能的職責單一化將會使我們避免很多生命周期相關的問題。Android 係統可能會隨時由於用戶的行為或者係統狀態(比如剩餘內存過低)而銷毀你的應用組件。所以應該最小化應用組件之間的依賴以提供一個健壯的體驗。
第二個重點是我們應該采用數據模型驅動 UI 的方式,最好是一個可持久化的模型。持久化被建議的原因有兩個:
用戶不會因為係統銷毀我們的應用而導致丟失數據。
我們的應用可以在網絡狀況不好甚至斷網的情況下繼續工作。
這裏說的模型其實也是一種組件,他們就是專門負責為我們的應用處理和存儲數據的。他們完全獨立於 Views 和其他應用中的組件,所以他們不存在生命周期相關的問題。保證 UI 部分的代碼足夠簡單,沒有業務邏輯,使代碼更容易去管理。
建議架構
在這部分,會用一個例子去說明怎麼使用新的 Android Architecture Components 去構建一個應用。
Note:
不可能找到一個完美的方案使用於所有場景。這裏的建議架構也隻是對於大部分場景來說應該是一個好的開始。但是如果你已經有一個更好的方案去設計你的應用,你可以繼續你的方案。
設想我們正在開發一個界麵,界麵是展示一個用戶信息。用戶信息會從我們自己的後台服務器通過一個 REST API 拉取。
構建用戶界麵
用戶界麵將由一個Fragment (UserProfileFragment.java)和對應的布局文件 user_profile_layout.xml 組成。
我們的數據模型需要持有兩個數據元素。
User ID: 要展示信息的用戶的 id。最好是通過一個 Fragment 的參數傳遞給 Fragment。如果 Android 係統銷毀我們的進程,這個信息將可以被存儲起來,所以在下一次我們的應用重新啟動時這個 id 是可以得到的。
User Object:一個普通的 POJO,裏麵封裝了 User 的信息屬性。
我們創建一個派生於 ViewModel 的 UserProfileViewModel 來保存上麵提到的兩個數據元素。
ViewModel 為特定的 UI Components 提供數據,比如 Fragment 或者 Activity,而且還負責與數據的業務邏輯通信,比如調用其他的組件去加載數據。ViewModel 與 View 解耦,並且不受配置改變的影響,比如由於旋轉屏幕導致的重新創建 Activity。
現在我們有3個文件:
user_profile.xml: 布局文件。
UserProfileViewModel.java: 負責給 UI 準備數據。
UserProfileFragment.java: 展示 ViewModel 提供的數據,並且負責與用戶的界麵交互。
下麵我們開始實現代碼:
UserProfileViewModel.java
UserProfileFragment.java
Note:
上麵的例子派生的是 LifecycleFragment 而不是 Fragment。在 Android Architecture Components 穩定後,Fragment 將直接實現 LifecycleOwner。
現在我們怎麼去將他們之間聯係起來呢?畢竟當 UserProfileViewModel 的 user 被設置的時候,需要有方法去通知 UI。這時候就是 LiveData 大顯身手的時候了。
LiveData 持有可被觀察的數據(其實就是我們這裏的 UserProfileViewModel 中持有的數據)。它使應用中的組件能夠在不與其存在明顯依賴關係的前提下觀察 LiveData 對象的改變。LiveData 遵從應用組件的生命周期狀態,並且能夠做一些事情去阻止對象內存泄漏。詳情參閱 LiveData。
Note:
如果你已經用了類似 RxJava 或 Agera 的庫,你可以繼續使用他們。但是你得確保你正確的處理生命周期問題。
現在我們將 UserProfileViewModel 中的 user 成員修改為 LiveData
UserProfileViewModel.java現在修改 UserProfileFragment,讓它可以觀察數據的改變並更新 UI。
UserProfileFragment.java
當 User 數據更新,onChanged 回調將會被調用進而刷新 UI。
如果你對其他使用觀察者回調的庫比較熟悉,可能你已經意識到我們沒有重寫 Fragment 的 onStop() 方法去停止觀察數據。這不是必要的對於 LiveData,因為 LiveData 是可感知生命周期的,這意味著它不會調用回調方法除非 Fragment 是處於激活狀態的(即收到 onStart() 但沒有收到 onStop())。LiveData 也會自動的刪除觀察者當 Fragment 調用 onDestory() 時。
我們也不用做任何事情去處理配置改變事件(比如屏幕旋轉)。ViewModel 將在配置改變的時候自動存儲,當一個新的 Fragment 到來,它將收到與配置改變前的 ViewModel 同樣的一個實例,而且 ViewModel 的回調方法將用該 ViewModel 內部持有數據做參數立馬調用。這就是為什麼 ViewModel 不應該直接引用 View,因為他的生命周期超出 View 的生命周期。
到這裏可能會有人分不太清 LiveData 與 ViewModel 的區別,我稍微總結一下。
ViewModel:它是一個組件模塊,是專門用來保存數據的,由 ViewModelProvider 來管理的。它的生命周期如圖:
如圖所述,它將一直保存在內存中除非 Activity 主動的 finish 或者 Fragment 被 detached。它不會受配置改變(如屏幕旋轉)的影響。
LiveData:它是一個可以讓數據具備可觀察功能的類,它不存在生命周期一說,隻是當 Activity 或者 Fragment 作為一個觀察者向它注冊後,它能夠感知 Activity 或 Fragment 的生命周期,並在相應的狀態下做相應的處理。
獲取數據
現在我們已經將 ViewModel 和 Fragment 聯係起來了,但是 ViewModel 怎麼去獲取數據呢?在我們這個例子中,我們假設我們的後台服務器提供了一套 REST API。我們可以使用 Retrofit 庫來訪問我們的後台服務器,你也可以選擇不同的庫實現同樣的目的。
下麵是我們基於 Retrofit 的用於和後台通信的 WebService:
Webservice.javaViewModel 內部可以直接調用 WebService 去獲取數據並且將數據分配到 User 對象中。雖然這能夠正常工作,但應用將再擴展後變得很難維護。這麼做將太多的事情放到了 ViewModel 中,違背了關注點分離的原則。另外,上麵提到 ViewModel 的生命周期和 Activity 或者 Fragment 是綁定在一起的,所以這麼做將在生命周期結束後丟失所有數據,這是一個很糟糕的體驗。所以正確的做法應該是,ViewModel 把這部分工作交給一個新的模塊去完成,Repository。
Repositore 模塊負責處理數據操作。它提供一套簡介清晰的 API 去簡化你的應用。它知道從哪去獲取數據,也知道當數據更新時調用什麼 API。你可以認為它是一個不同數據源之間的中間人(比如數據庫數據源,網絡數據源,Memory Cache 數據源等)。
接下來我們就定義 UserRepository 類,它將通過 WebService 來獲取數據:
UserRepository.java雖然 Repository 模塊好像不是必要的,但是它的存在有重要的意義。針對應用上層來說它抽象了數據源。現在我們使用 ViewModel 的時候並不知道數據的獲取是通過 WebService 獲取的,上層也不需要關心。這意味著我們能夠在必要的時候使用其他的實現來獲取數據而不用修改上層代碼(比如添加了 Cache 數據源,持久化數據源)。
我們這裏忽略了網絡異常的情況。
管理組建間的依賴
上麵提到的 UserRepository 需要一個 WebService 的實例去完成它的工作。它可能實現起來比較簡單,但是這會帶來更多的依賴,比如在 UserRepository 內部去構造 WebService 時需要知道 WebService 構造函數的參數有哪些,也就是需要知道 WebService 依賴了哪些模塊,導致 WebService 依賴的模塊間接也與 UserRepository 產生了依賴,這將會很複雜並且帶來很多重複的代碼。另外,UserRepository 可能不是唯一的需要 WebService 的類,如果每一個需要 WebService 的類都這麼去創建使用它,這個工作將非常惡心。
這裏有兩個方案去解決這個問題:
依賴注入(Dependency Injection):依賴注入可以讓類去定義他們的依賴實例,但不用構造它們。在運行時其他的類將負責提供這些依賴。這裏建議 Google 的 Dagger 2 庫去實現依賴注入。
服務定位器(Service Locator):服務定位器提供了一個注冊表,類能夠在其中獲取他們的依賴,所以不用去構造他們。服務定位器相對依賴注入來說要簡單一些,所以如果你對依賴注入不是太熟悉,可以使用服務定位器。具體可參閱 Service Locator。
在這個例子中我們使用 Dagger 2 來管理依賴關係。
ViewModel和Repository
現在我們修改 UserProfileViewModel 去使用 Repository:
UserProfileViewModel.java
持久化數據
我們在上麵做了 Memory Cache,所以如果用戶旋轉屏幕或者離開之後返回應用,隻要應用沒有被 kill,那麼界麵將立馬展現出來。因為 Repository 能夠從 Memory Cache 中將數據恢複出來。但是如果用戶離開應用很久,在 Android 係統殺了應用後才返回會發生什麼呢?
如果按照當前的實現,我們將重新從網絡獲取數據。這不隻是一個糟糕的體驗,也是一種浪費,因為它可能會用移動流量去重新獲取同樣的數據。
正確的方法去處理這種問題是使用一個持久化模型,Room 持久化庫能夠拯救你。Room 詳細信息可參閱 Room。
為了使用 Room,我們需要定義一些本地的規則。首先,用 @Entity 注解去標注 User 類,表明它將作為數據庫中的一張表。
User.java
然後派生 RoomDatabase類創建一個我們的數據庫類:
MyDatabase.java
注意 MyDatabase 是一個抽象類,Room 會為它自動提供一個實現。詳細可參閱 Room文檔。
現在我們需要一個方法插入數據到數據庫中。因此我們創建一個數據訪問對象(DAO)。
UserDao.java
然後從我們的數據庫類中引用上麵定義的 DAO 類:
MyDatabase.java注意,加載數據的方法返回的是一個 LiveData
Note:
在目前的版本中,Room 基於數據表修改的檢查是無效的,這意味這意味著它可能會派發一些錯誤的通知。
現在我們修改UserRepository 去加入 Room數據源:
UserRepository.java這裏雖然我們改變了數據獲取的來源,但我們不需要修改 UserProfileViewModel 或者 UserProfileFragment。這是抽象帶來的靈活性。這也是一個很棒的一點針對測試來說,因為我們可以提供一個測試用的 UserRepository 來測試 UserProfileViewModel。這也是麵對抽象(麵對接口)編程的優勢。
現在我們的代碼基本完成。如果用戶在幾天之後回到同樣的界麵,他們可以立即看到界麵信息因為我們已經將數據持久化到數據庫了。與此同時,如果數據是太老了,Repository 會在後台更新數據。當然著決定與你應用的使用場景,有可能你覺得持久化的數據太老的話不顯示在界麵上反而更好。
單一數據源
Single source of truth
在複雜的業務數據結構的情況下,我們常常會遇到不同的 REST API 返回了同樣的數據,如果不同的 component 的顯示直接依賴於 API 數據的返回,很有可能就會有不同的 component 對於同樣的數據所顯示的結果不一樣的 bug。再加上緩存、用戶修改數據等等複雜情況,顯示不一致的問題可能更加嚴重,所以為了解決這個問題,Single source of truth(以下使用中文名稱:單一數據源)的概念被提出來了。
在我們額模型中,數據庫扮演了一個單一數據源的角色,應用的其他部分能夠通過 Repository 去訪問數據庫。忽略你是否使用了磁盤緩存,我們建議你的 Repository 應該要指定一個數據源作為單一數據源。
測試
我們已經提過模塊分離的一個好處是提高程序的易測性。下麵就來談談怎麼去測試我們的每一個代碼模塊。
界麵和交互:這裏你可能需要 Android UI Instrumentation test 的幫助。最好的測試UI的方法就是實用 Espresso 測試框架。你隻需要創建一個 mock 的 ViewModel,因為與 Fragment 通信的模塊隻有 ViewModel。
ViewModel: ViewModel 可以通過 JUnit test 來測試。你隻需要 mock 一個 UserRepository 就可以完成測試。
UserRepository: 也可以實用 JUnit test 來測試。這裏需要 mock 住 WebService 和 DAO 類。你可以給一個正確的網絡接口讓程序調用,然後去測試整個流程,包括將結果保存入庫等。因為 WebService 和 UserDao 都是接口,所以除了可以 mock 它們,還可以通過實現接口去創建更多的複雜測試場景。
UserDao: 這裏建議針對 DAO 類使用 instrumentation test 測試。因為 instrumentation test 不需要任何 UI,它們運行的很快。針對每一個測試,都可以創建一個 in-memory 的數據庫去確保測試不會受其他方麵的響(比如磁盤文件的改變)。
Room 支持指定特定的數據庫實現,所以你可以提供一個 SupportSQLiteOpenHelper 的實現去完成單元測試。但是這個方法通常不建議,因為你無法保證 SQLite 的版本在運行的設備和你的主機上是一致的。
WebService: 對於測試該模塊來說獨立與其他的模塊場景是很重要的,甚至在單元測試時你應該避免它與你的後台發生網絡通信。有很多庫能幫助你完成這工作。比如,MockWebServer 是一個很棒的庫能幫助你創建一個虛擬的本地服務來測試。
最終架構
下麵這張圖展示了各個模塊的架構以及它們之間是如何交互的:指導原則
下麵的建議不是強製性的,不過從經驗上看,隨著你的代碼長期迭代,遵循下麵的準則來完成你的編程工作將可以使代碼在健壯性,易測試性,可維護性方麵都更加優秀。
你定義在 Manifest 中的 entry points(比如 Activity,Service,BroadcastReceiver)都不應該作為數據源。他們應該隻去使用與他們的相關數據子集。因為這些組件的生命周期太短了,如果讓他們持有完整的數據源,在他們被銷毀後整個數據源將全部銷毀。
嚴格的定義好各個模塊間的邊界。比如,不要將網絡請求相關的代碼覆蓋到多個包或者類中,類似的,也不要將不相關的一些責任揉在一起。比如不要將數據的緩存和綁定放到同一個類裏麵。做到模塊職責單一化。
模塊間要盡量做到低耦合,不要嚐試為了一點點的方便而將一個模塊的內部實現細節暴露出來。你可能在短時間內會覺得很方便,但你的代價是在你代碼的迭代演進過程中將花費更多的時間去維護它。
當你在定義模塊間的交互行為時,應該去思考怎麼樣設計他們,彼此之間才可以獨立的完成單元測試。
不要把你的時間花在重複造輪子或者重複寫同樣的模板代碼上,相反的,你的主要經曆應該聚焦在怎麼使你的應用變得獨一無二。讓 Android Architecture Components 和其他建議的庫 來處理這些重複的工作。
盡可能多的存留一些相關的新的數據在本地,為了讓你的應用在設備處於離線狀態下時仍然可用。要知道你可能享受著持續穩定高速的網絡連接,但是你的用戶不一定。
你的 Repository 應該指明一個數據源作為單一數據源。不管什麼時候你的應用需要訪問一些數據,它應該都可以從單一數據源中找到。
---------------------------------------------
如果您覺得內容不錯
請點擊屏幕右上角按鈕【分享到朋友圈】分享內容
更多精彩內容請關注公眾號:tx_cdc
没有评论:
发表评论