【譯】Android應用架構

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要做到以下幾個步驟:

  1. 通過APIProvider調用loadPosts方法(回調)

  2. 等待APIProvider的回調結果,然后調用CacheProvider中的savePosts方法(回調)

  3. 等待CacheProvider的回調結果,然后將這些文章展示到ListView等列表視圖上

  4. 分別處理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 LayerView LayerData Layer持有DataManager和一系列的Helper classeView 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方法可能的樣子。這個簡單的示例方法如下:

  1. 調用Retrofit service從REST API加載一個博客文章列表

  2. 使用DatabaseHelper保存文章到本地數據庫,達到緩存的目的

  3. 篩選出今天發表的博客,因為那才是View Layer想要展示的。

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);
                        }
                    });
    }

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社區內已經越來越受歡迎了。通過在[示例工程]( sample project)和文章中進行探索后,我們發現MVP,可能給我們現有的架構帶來非常價值的改進。因為當前我們的架構已經被劃分為兩個層(視圖層和數據層),添加MVP會更自然些。我們只需要添加一個新的presenter層,然后將View中的部分代碼轉移到presenter就行了。

留下的Data Layer保持不變,只不過為了與這種模式保持一致性,它現在被叫做Model

Presenter負責從Model中加載數據,然后當數據準備好之后調用View中相對應的方法。還負責訂閱DataManager返回的Observable。所以,他們還需要處理schedulerssubscriptions。此外,它們還能分析錯誤代碼或者在需要的情況下為數據流提供額外的操作。例如,如果我們需要過濾一些數據而且這個相同的過濾器是不可能被重用在其他地方的,這樣的話在Presenter中實現比在DataManager中或許更有意義。

下面你將看到在Presenter中一個public方法將是什么樣子。這段代碼訂閱我們在前一節中定義的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是與Presenter一起協助的View組件。通常情況下是一個ActivityFragment或者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應用程序。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容