Android應用架構變更背後的經驗、失誤與推論

CSDN發表於2015-12-18

軟體程式碼庫各個不同的部分應當彼此獨立,其整體卻猶如一部運轉良好的機器

Android的開發生態系統發展迅速,每週都有變化,人們不停地建立新工具、更新資源庫、撰寫博文、發表演講。只要享受一個月的假期,回來的時候支援庫和/或Play Services都更新換代了。

Android應用架構變更背後的經驗、失誤與推論

筆者與 ribot團隊 合作開發Android應用已有超過三年時間。在這段時間裡,我們用來構建Android應用的架構與技術一直在不斷進化。在本文中,我們將具體闡述這些架構變更背後的經驗、失誤還有推論。

過去

早在2012年,我們的程式碼庫總是採用基礎架構,並未使用任何網路庫,還是用老一套的AsyncTasks。下面的圖表粗略地演示了這個架構。

Android應用架構變更背後的經驗、失誤與推論

初始架構

程式碼共分兩層:控制從REST API檢索/儲存資料的資料層(data layer),還有負責在UI上控制與展示資料的檢視層(view layer)。

APIProvider提供方法,讓Activities和Fragments能夠很容易地與REST API互動。運用URLConnection和AsyncTasks來執行單獨執行緒中的網路呼叫,並通過回撥向Activities返回結果。

類似地,CacheProvider包含了從SharedPreferences或SQLite資料庫檢索儲存資料的方式,通過回撥將結果返回給Activities。

問題

這個辦法的主要問題在於,檢視層責任過大。試想一個簡單的通用場景:應用程式在載入文章列表時,將其快取到SQLite資料庫中,並最終展示在ListView中。具體執行如下:

  1. 呼叫APIProvider中的loadPosts(回撥)方法;
  2. 等待APIProvider成功回撥,然後呼叫CacheProvider中的savePosts(回撥);
  3. 等待CacheProvider成功回撥,然後在ListView中顯示文章;
  4. 分別處理APIProvider和CacheProvider的回撥錯誤。

這是個簡單的例子。在真實案例場景中,REST API可能不會按照瀏覽所需的那樣返回資料,因此Activity會設法在展示資料之前對其進行轉換或過濾。另一個常見案例:在使用 loadPosts() 方法獲取需要從別處拿到的引數時,比如由Play Services SDK提供的電子郵件地址,很有可能SDK會通過回撥非同步返回郵件,也就是說我們現在有三層巢狀回撥(nested callbacks)。如果複雜性繼續增加,這個方法會導致所謂的回撥地獄(callback hell)。

總結:

  • Activities和Fragments逐漸過大而難以維護;
  • 巢狀回撥太多,導致程式碼醜陋不堪,難以理解與修改,也不好增加新功能;
  • 單元測試也頗有難度,即便勉強進行,由於Activities或Fragments中包含有大量邏輯,相關工作也會相當費勁。

由RxJava驅動的新架構

差不多在兩年時間中,我們都在採用前面描述的那種架構。在那段時間裡,我們做了一些修正,但是解決問題時收效甚微。例如,我們增加了一些helper類,以減少Activities和Fragments中的程式碼,並開始在APIProvider中使用 Volley 。儘管如此,在應用程式碼測試時還是面臨測試友好性問題與回撥地獄頻繁出現的問題。

直到2014年我們發現了 RxJava ,在嘗試了幾個樣例專案後,我們發現這可能是解決巢狀回撥問題的終極解決辦法。如果對響應式程式設計不熟悉的話,可以參考 這篇簡介 。簡單來講,RxJava允許使用者通過非同步流管理資料,並提供很多可用在事件流中的 operator ,方便使用者修改、篩選或合併資料。

考慮到前些年遭受的痛苦,我們開始考慮新應用的架構是什麼樣的,然後得出了這個。

Android應用架構變更背後的經驗、失誤與推論

與頭一個方法類似,這個架構也可以分為兩層,分別是資料層與檢視層。資料層包含DataManager,還有一系列helper。檢視層由諸如Fragments、Activities、ViewGroups等Android框架元件構成。

Helper類(圖表第三列)包含具體的職責,同時執行方式也很簡潔。例如大多專案包含訪問REST API的helper,從資料庫讀取資料的helper或者與第三方SDK互動的helper。不同的應用程式包含不同數量的helper,不過最常見的helper有:

  • PreferencesHelper:在SharedPreferences中讀取與儲存資料。
  • DatabaseHelper:處理SQLite資料庫的訪問。
  • Retrofit 服務:從REST API執行呼叫。我們使用Retrofit來代替Volley,因為它提供了對RxJava的支援,也更好用。

大多數helper類中的公共方法會返回RxJava Observables。

DataManager是這個架構的核心,它廣泛運用了RxJava operator來合併、篩選與轉換從helper類中獲得的資料。DataManager的目標是通過提供準備顯示的資料,來減少Activities 和Fragments的工作量,而且這些資料一般無需任何轉換。

下面的程式碼就是DataManager方法的例項。

  • 呼叫Retrofit服務來載入從REST API獲取的文章列表。
  • 用DatabaseHelper在本地資料庫中儲存文章,做快取使用。
  • 按照檢視層的需求,篩選出今天撰寫的文章。
public Observable<Post> loadTodayPosts() {
            return mRetrofitService.loadPosts()
                    .concatMap(new Func1<List<Post>, Observable<Post>>() {
                        @Override
                        public Observable<Post> call(List<Post> apiPosts) {
                            return mDatabaseHelper.savePosts(apiPosts);
                        }
                    })
                    .filter(new Func1<Post, Boolean>() {
                        @Override
                        public Boolean call(Post post) {
                            return isToday(post.date);
                        }
                    });
    }

像Activities或Fragments之類的檢視層元件會簡單呼叫這個方法,並訂閱返回的Observable。一旦訂閱完成,Observable所發出的不同文章就能直接加入到Adapter中,以便在RecyclerView或類似元件中顯示。

這個架構的最後一個元素是Eventbus(事件匯流排),它允許我們將資料層的事件進行廣播,因此檢視層的多個元件能夠訂閱這些事件。例 如,DataManager中的signOut()方法可以在Observable完成時釋出一個事件,讓多個訂閱這個事件的Activities修改 UI,顯示為登出狀態。

為什麼這個方法更好?

RxJava Observables和operators使得巢狀回撥不再有必要。

Android應用架構變更背後的經驗、失誤與推論

  • DataManager接管了之前檢視層的部分職責,從此Activities和Fragments更為輕量。
  • 將程式碼從Activities和Fragments中轉移到DataManager和helpers中,意味著單元測試寫起來更簡單。
  • 明確的職責分離,加上使用DataManager作為唯一與資料層的互動點,這些做法讓這個架構測試時更為友好。Helper類或DataManager很容易模擬。

還有什麼問題呢?

  • 對於非常複雜的大型專案來說,DataManager可能會過於龐大而難以維護。
  • 儘管Activities與Fragments之類的檢視層元件逐漸更為輕量級,仍然需要處理相當數量的邏輯,比如管理RxJava訂閱、分析錯誤等。

整合模型檢視顯示

在過去的一年中,像MVP、MVVM這樣的一些架構模型在Android社群受到了熱捧。在 樣例專案文章 中研究過這些模型之後,我們發現MVP能夠對我們目前的方法帶來很有價值的改進。由於我們目前的架構分為兩層(檢視與資料層),加上MVP也很自然。我們 只需增加一個新的展示層(a new layer of presenters),將一部分程式碼從檢視層移過去就可以了。

Android應用架構變更背後的經驗、失誤與推論

基於MVP的架構

資料層保持不變,不過現在改名為模型層(Model),以便名符其實。

展示層控制載入來自模型層的資料,並在結果準備好之後呼叫檢視層的正確方法來顯示。它訂閱DataManager返回的Observables,因此必須處理類似 排程 與 訂閱 之類的工作。此外,它可以分析錯誤程式碼,或者在需要時在資料流中應用額外操作。例如,如果我們需要篩選一些資料,而這個篩選無法在其他地方複用,那麼用展示層來實現會比在DataManager實現要更好。

下面是在展示層中公共方法的案例。這部分程式碼訂閱了從dataManager.loadTodayPosts()方法返回的Observable。

public void loadTodayPosts() {
    mMvpView.showProgressIndicator(true);
    mSubscription = mDataManager.loadTodayPosts().toList()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe(new Subscriber<List<Post>>() {
                @Override
                public void onCompleted() {
                    mMvpView.showProgressIndicator(false);
                }

                @Override
                public void onError(Throwable e) {
                    mMvpView.showProgressIndicator(false);
                    mMvpView.showError();
                }

                @Override
                public void onNext(List<Post> postsList) {
                    mMvpView.showPosts(postsList);
                }
            });
    }

mMvpView是這個展示層正在assist的檢視層元件。一般MVP檢視是Activity、Fragment或ViewGroup例項。

就像之前的架構那樣,檢視層包含像ViewGroups、Fragments或Activities這樣的標準框架元件。這些元件的主要區別在於 沒有直接訂閱Observables,而是執行MVP檢視,提供一系列類似showError() 或showProgressIndicator()之類的簡明方法。檢視元件還控制處理類似點選事件之類的與使用者互動,並通過呼叫展示層的正確方法來執 行。例如,如果我們有一個載入文章列表的按鈕,Activity就會從onClick監聽那裡呼叫 presenter.loadTodayPosts()。

想要檢視基於MVP的完整架構,請檢視 Android Boilerplate project on GitHub 或者 ribot’s architecture guidelines

為什麼這個方法更好?

  • Activities和Fragments都很輕量。只需負責建立/更新UI,處理使用者事件。因此更容易維護。
  • 我們現在能夠通過模擬檢視層,從展示層書寫簡單的單元測試了。之前這些程式碼是檢視層的一部分,沒辦法進行單元測試。而且整體架構對測試更加友好。
  • 如果DataManager過於龐大,我們可以通過將一些程式碼挪到presenter中緩解這個問題。

還有什麼問題?

  • 在程式碼庫變得非常龐大與複雜時,單一的DataManager仍是個問題。我們尚未觸及到真實問題點,不過遲早會碰到。

需要注意的是,這個架構並不完美。事實上,認為它是唯一而且完美的架構,能夠一勞永逸的解決問題這樣的想法太過天真。Android的生態系統會繼續保持高速發展,我們必須持續探索、閱讀、實驗,才能找到構建優秀Android應用的更佳途徑。

相關文章