Android開發生態圈的節奏非常之快。每週都會有新的工具誕生,類庫的更新,部落格的發表以及技術探討。如果你外出度假一個月,當你回來的時候可能已經發布了新版本的Support Library或者Play Services
我與Ribot Team一起做Android應用已經超過三年了。這段時間,我們所構建的Android應用架構和技術也在不斷地演變。本文將向您闡述我們的經驗,錯誤以及架構變化背後的原因。
曾經的架構
追溯到2012年我們的程式碼庫使用的是基本結構,那個時候我們沒有使用任何第三方網路類庫,而且AsyncTask
也是我們的好朋友。當時的架構可以大致表示為下圖。
程式碼被劃分為兩層結構:Data Layer(資料層)負責從REST API或者持久資料儲存區檢索和儲存資料;View Layer(檢視層)的職責是處理並將資料展示在UI上。
APIProvider提供了一些方法,使Activity和Fragment能夠很容易的實現與REST API的資料互動。這些方法使用URLConnection和AsyncTask在一個單獨的執行緒內執行網路請求,然後通過回撥將結果返回給Activity。
按照同樣的方式,CacheProvider 所包含的方法負責從SharedPreferences和SQLite資料庫檢索和儲存資料。同樣使用回撥的方式,將結果傳回Activity。
存在的問題:
使用這種結構,最主要的問題在於View Layer持有太多的職責。想象一個簡單且常見的場景,應用需要載入一個部落格文章列表,然後快取這些條目到SQLite資料庫,最後將他們展示到ListView等列表檢視上。Activity要做到以下幾個步驟:
- 通過APIProvider呼叫
loadPosts
方法(回撥) - 等待APIProvider的回撥結果,然後呼叫CacheProvider中的
savePosts
方法(回撥) - 等待CacheProvider的回撥結果,然後將這些文章展示到ListView等列表檢視上
- 分別處理APIProvider和CacheProvider回撥中潛在的異常。
這是一個非常簡單的例子,在實際開發環境中REST API返回的資料可能並不是View直接需要的。因此,Activity在進行展示之前不得不通過某種方式將資料進行轉換或過濾。另一個常見的情況是,呼叫loadPosts( )
所需要的引數,需要事先從其他地方獲取到,比如,需要Play Services SDK提供一個Email地址引數。就像SDK通過非同步回撥的方式返回Email地址,這就意味著現在我們至少有三層巢狀的回撥。如果繼續新增複雜的業務邏輯,這種架構就會陷入眾所周知的Callback Hell(回撥地獄)。
總結:
- Activitty和Fragment變得非常龐大並且難以維護。
- 太多的回撥巢狀意味著醜陋的程式碼結構而且不易讀懂和理解。如果在這個基礎上做更改或者新增新特性會感到很痛苦。
- 單元測試變得非常有挑戰性,如果有可能的話,因為很多邏輯都留在了Activity或者Fragment中,這樣進行單元測試是很艱難的。
RxJava驅動的新型架構
我們使用上文提到的組織架構差不多兩年的時間。在那段時間內,我們做了一些改進,稍微緩解了上述問題。例如,我們新增了一些Helper Class(幫助類)用來減少Activity和Fragment中的程式碼,在APIProvider中使用了Volley。儘管做出了這些改變,我們應用程式的程式碼還是不能進行友好的測試,並且Callback Hell(回撥地獄)的問題還是經常發生。
直到2014年我們開始瞭解RxJava。在嘗試了幾個示例專案之後,我們意識到她可能最終幫助我們解決掉巢狀回撥的問題。如果你還不熟悉響應式程式設計,可以閱讀本文(譯者注:譯文點這裡那些年我們錯過的響應式程式設計)。簡而言之,RxJava允許通過非同步流的方式處理資料,並且提供了很多操作符,你可以將這些操作符作用於流上從而實現轉換,過濾或者合併資料等操作。
考慮到經歷了前幾年的痛苦,我們開始考慮,一個新的應用程式體系架構看起來會是怎樣的。因此,我們想出了這個。
類似於第一種架構,這種體系架構同樣被劃分為Data Layer和View Layer。Data Layer持有DataManager
和一系列的Helper classe 。View Layer由Android的Framework元件組成,例如,Fragment,Activity,ViewGroup等。
Helper classes(圖示中的第三列)有著非常特殊的職責以及簡潔的實現方式。例如,很多專案需要一些幫助類對REST API進行訪問,從資料庫讀取資料,或者與三方SDK進行互動等。不同的應用擁有不同數量的幫助類,但也存在著一些共性:
- PreferencesHelper:從
SharedPreferences
讀取和儲存資料。 - DatabaseHelper:處理操作SQLite資料庫。
- Retrofit services:執行訪問REST API,我們現在使用Retrofit來代替Volley,因為它天生支援RxJava。而且也更好用。
幫助類裡面的大多數public方法都會返回RxJava的Observable
。
DataManager是整個架構中的大腦。它廣泛的使用了RxJava的操作符用來合併,過濾和轉換從幫助類中返回的資料。DataManager旨在減少Activity和Fragment的工作量,它們(譯者注:指Activity和Fragment)要做的就是展示已經準備好的資料而不需要再進行轉換了。
下面這段程式碼展示了一個DataManager方法可能的樣子。這個簡單的示例方法如下:
- 呼叫Retrofit service從REST API載入一個部落格文章列表
- 使用DatabaseHelper儲存文章到本地資料庫,達到快取的目的
- 篩選出今天發表的部落格,因為那才是View Layer想要展示的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public Observable loadTodayPosts() { return mRetrofitService.loadPosts() .concatMap(new Func1, Observable>() { @Override public Observable call(List apiPosts) { return mDatabaseHelper.savePosts(apiPosts); } }) .filter(new Func1() { @Override public Boolean call(Post post) { return isToday(post.date); } }); } |
在View Layer中諸如Activity或者Fragment等元件只需呼叫這個方法,然後訂閱返回的Observable
即可。一旦訂閱完成,通過Observable傳送的不同部落格,就能夠立即被新增進Adapter從而展示到RecyclerView或其他類似控制元件上。
這個架構的最後元素就是Event Bus(事件匯流排)。它允許我們在Data Layer中傳送事件,以便View Layer中的多個元件都能夠訂閱到這些事件。比如DataManager中的退出登入方法可以傳送一個事件,訂閱這個事件的多個Activity在接收到該事件後就能夠更改它們的UI檢視,從而顯示一個登出狀態。
為什麼這種架構更好?
- RxJava的Observable和操作符避免了巢狀回撥的出現。
- DataManager接管了以前View Layer的部分職責。因此,它使Activity和Fragment變得更輕量了。
- 將程式碼從Activity和Fragment轉移到了DataManager和幫助類中,就意味著使寫單元測試變得更簡單。
- 明確的職責分離和DataManager作為唯一與Data Layer進行互動的點,使這個架構變得Test-Friendly。幫助類和DataManager能夠很容易的被模擬出來。
我們還存在什麼問題?
- 對於龐大和複雜的專案來講,DataManager會變得非常的臃腫和難以維護。
- 儘管View Layer諸如Activity和Fragment等元件變得更輕量,它們讓然要處理大量的邏輯,如管理RxJava的訂閱,解析錯誤等方面。
整合MVP
在過去的一年中,幾個架構設計模式,如MVP或者MVVM在Android社群內已經越來越受歡迎了。通過在示例工程和文章中進行探索後,我們發現MVP,可能給我們現有的架構帶來非常價值的改進。因為當前我們的架構已經被劃分為兩個層(檢視層和資料層),新增MVP會更自然些。我們只需要新增一個新的presenter層,然後將View中的部分程式碼轉移到presenter就行了。
留下的Data Layer保持不變,只不過為了與這種模式保持一致性,它現在被叫做Model。
Presenter負責從Model中載入資料,然後當資料準備好之後呼叫View中相對應的方法。還負責訂閱DataManager返回的Observable。所以,他們還需要處理schedulers和subscriptions。此外,它們還能分析錯誤程式碼或者在需要的情況下為資料流提供額外的操作。例如,如果我們需要過濾一些資料而且這個相同的過濾器是不可能被重用在其他地方的,這樣的話在Presenter中實現比在DataManager中或許更有意義。
下面你將看到在Presenter中一個public方法將是什麼樣子。這段程式碼訂閱我們在前一節中定義的dataManager.loadTodayPosts( )
所返回的Observable。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public void loadTodayPosts() { mMvpView.showProgressIndicator(true); mSubscription = mDataManager.loadTodayPosts().toList() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Subscriber>() { @Override public void onCompleted() { mMvpView.showProgressIndicator(false); } @Override public void onError(Throwable e) { mMvpView.showProgressIndicator(false); mMvpView.showError(); } @Override public void onNext(List postsList) { mMvpView.showPosts(postsList); } }); } |
mMvpView
是與Presenter一起協助的View元件。通常情況下是一個Activity
,Fragment
或者ViewGroup
的例項。
像之前的架構,View Layer持有標準的Framework元件,如ViewGroup,Fragment或者Activity。最主要的不同在於這些元件不再直接訂閱Observable。取而代之的是通過實現MvpView介面,然後提供一些列簡潔的方法函式,比如showError( )
或者showProgressIndicator( )
。這個View元件也負責處理使用者互動,如點選事件和呼叫相應Presenter中的正確方法。例如,我有一個按鈕用來載入部落格列表,Activity將會在點選事件的監聽中呼叫presenter.loadTodayPosts( )
如果你想看到一個完整的運用MVP基本架構的工作示例,可以從Github檢出我們的Android Boilerplate project。也可以從這裡閱讀關於它的更多資訊Ribot的架構指導
為什麼這種架構更好?
- Activity和Fragment變得非常輕量。他們唯一的職責就是建立/更新UI和處理使用者事件。因此,他們變得更容易維護。
- 現在我們通過模擬View Layer可以很容易的編寫出單元測試。之前這些程式碼是View Layer的一部分,所以我們很難對它進行單元測試。整個架構變得測試友好。
- 如果DataManager變得臃腫,我們可以通過轉移一些程式碼到Presenter來緩解這個問題。
我們依然存在哪些問題?
- 當程式碼庫變得非常龐大和複雜時,單一的DataManager依然是一個問題。雖然我們還沒有走到這一步,但這是一個真正值得注意的問題,我們已經意識到了這一點,它可能發生。
值得一提的是它並不是一個完美的架構。事實上,不要天真的認為這是一個獨特且完美的方案,能夠解決你所有的問題。Android生態系統將保持快速發展的步伐,我們必須繼續探索。不斷地閱讀和嘗試,這樣我們才能找到更好的方法來繼續構建優秀的Android應用程式。