Android App 架構(gòu)演變

最近App項目(MVC架構(gòu))越做越大脏榆,協(xié)同開發(fā)效率較低,維護困難台谍,所以產(chǎn)生了調(diào)整架構(gòu)的想法须喂,在 簡書、csdn趁蕊、知乎上看了不少文章坞生,感覺知乎用戶 0x8421bcd 對于“Android項目開發(fā)如何設(shè)計整體架構(gòu)?”的回答頗為精彩掷伙,在此引用是己,鞠躬感謝谎倔!

0. 前言

想要設(shè)計App的整體框架沥邻,首先要清楚我們做的是什么乳愉。一般我們與網(wǎng)絡(luò)交互數(shù)據(jù)的方式有兩種:主動請求(http)和長連接推送觉阅。
結(jié)合網(wǎng)絡(luò)交互數(shù)據(jù)的方式來說一下我們開發(fā)的App的類型和特點:

  • 數(shù)據(jù)展示類型的App
    特點是頁面多饥悴,需要頻繁調(diào)用后端接口進行數(shù)據(jù)交互用爪,以http請求為主顽照;推送模塊无畔,IM類型App的IM核心功能以長連接為主宅粥,比較看重電量参袱、流量消耗。
  • 手機助手類App
    主要著眼于系統(tǒng)API的調(diào)用,達到輔助管理系統(tǒng)的目的蓖柔,網(wǎng)絡(luò)調(diào)用的方式以http為主辰企。
  • 游戲
    一般分為游戲引擎和業(yè)務(wù)邏輯,業(yè)務(wù)腳本化編寫况鸣,網(wǎng)絡(luò)以長連接為主牢贸,http為輔。

一般我們做的App都是類型1镐捧,簡要來說這類app的主要工作就是把服務(wù)端的數(shù)據(jù)拉下來給用戶展示潜索,把用戶在客戶端修改的數(shù)據(jù)上傳給服務(wù)端處理,所以這類App的網(wǎng)絡(luò)調(diào)用相當(dāng)頻繁懂酱,而且需要考慮到網(wǎng)絡(luò)差竹习、沒網(wǎng)絡(luò)等情況下App能夠正常運行。

成熟的商業(yè)應(yīng)用的網(wǎng)絡(luò)調(diào)用一般是如下流程:UI發(fā)起請求 -> 檢查緩存 -> 調(diào)用網(wǎng)絡(luò)模塊 -> 解析返回JSON / 統(tǒng)一處理異常 -> JSON對象映射為Java對象 -> 緩存 -> UI獲取數(shù)據(jù)并展示列牺,這之中可以看到很明顯職責(zé)劃分整陌,即:數(shù)據(jù)獲取瞎领;數(shù)據(jù)管理泌辫;數(shù)據(jù)展示。確定了職責(zé)九默,就可以進入正題了震放。

1. MVC架構(gòu)

Android最原生也是最基礎(chǔ)的架構(gòu),可以理解為MVC(Model-View-Controller)驼修,Controller即是Activity和Fragment殿遂,但是這兩者掌握了Android系統(tǒng)中絕大多數(shù)的資源,并且在內(nèi)部直接控制View乙各,因此MVC架構(gòu)一般是以Activity和Fragment為核心墨礁,將網(wǎng)絡(luò)模塊,數(shù)據(jù)庫管理模塊耳峦,文件管理模塊饵溅,常用工具類等分離成若干工具類包,供Activity和Fragment使用妇萄。


MVC Frame.png

這是比較基礎(chǔ)的Android項目架構(gòu)冠句,市面上大部分App都是這種造型。

  • 優(yōu)點
    開發(fā)簡單懦底,以頁面為導(dǎo)向;如果構(gòu)建水平可以聚唐,項目就已經(jīng)基本實現(xiàn)模塊化,基于Activity杆查、Fragment這兩個上帝般的存在,很多事情直接就妥了亲桦,不用繞。
  • 缺點
    維護難客峭,因為是以頁面為導(dǎo)向的,有些需要共用的業(yè)務(wù)邏輯就會很煩舔琅,don't repeat your self等恐, 你要不要repeat ?不想repeat就要寫模塊备蚓,慢慢的項目就會多出一堆亂七八糟的小模塊课蔬。另一方面,測試很困難星著,因為所有的數(shù)據(jù)處理都在Activity和Fragment购笆,假如現(xiàn)在想先用假數(shù)據(jù)顯示,就要直接改Activity和Fragment的數(shù)據(jù)控制邏輯虚循。還有個最惱火的問題同欠,那就是業(yè)務(wù)復(fù)雜起來后Activity和Fragment的代碼量激增,舉一個例子横缔,電商App的購物車铺遂,如果只是管理一下購物車中的商品,無非就是查茎刚、刪襟锐、改調(diào)用,列表管理膛锭,300多行代碼應(yīng)該就搞定了粮坞,假如現(xiàn)在加了個優(yōu)惠券提示呢?光優(yōu)惠券不夠初狰,還有滿減莫杈,還有湊單,要計算運費奢入。還要能領(lǐng)取優(yōu)惠券…… 噢筝闹,忘了一般來說還有一個商品推薦,好了現(xiàn)在有兩個列表要管理了,你覺得CartActivity 2000行代碼能止住么关顷?在上面這些缺點的描述中糊秆,可以看到一個很大的痛點在于:Activity和Fragment不應(yīng)該管這么多數(shù)據(jù)處理邏輯

2. 分層架構(gòu)(在MVC的基礎(chǔ)上分層)

如果仔細看自己的項目议双,可以發(fā)現(xiàn)絕大多數(shù)數(shù)據(jù)處理的代碼是不需要使用Activity和Fragment持有的資源的(比如Context)痘番,而很多時候我們需要多個頁面共用一套數(shù)據(jù)和請求邏輯,很經(jīng)典的例子是應(yīng)用中的User對象聋伦,一般來說都是全局單例夫偶。這些全局的數(shù)據(jù)源寫多了,很容易就能想到將數(shù)據(jù)處理統(tǒng)一抽出來形成一層觉增,向上層提供數(shù)據(jù)接口兵拢,而上層并不關(guān)心數(shù)據(jù)的來源(內(nèi)存,緩存说铃,網(wǎng)絡(luò))腻扇,因為不用從Activity和Fragment拿資源而且主要工作是數(shù)據(jù)處理幼苛,所以這一層是UI無關(guān)的舶沿,大幅提升了復(fù)用性括荡,我把這一層稱為DataManager層溉旋。
這是我一個項目的包結(jié)構(gòu):

MVC-Hierarchy.png

Activity和Fragment剝離了數(shù)據(jù)處理的責(zé)任后观腊,持有DataManager的引用梧油,負責(zé)獲取數(shù)據(jù)并展示,向DataManager傳遞數(shù)據(jù),絕不進行網(wǎng)絡(luò)請求和緩存讀寫迄委。
image.png

舉個栗子叙身,分頁加載。一般來說分頁加載接口返回的數(shù)據(jù)是這樣的:

{
    "code":0,
    "message":"success",
    "data":{
        "page":1,
        "totalPage":10,
        "pageSize":20,
        "total":200,
        "list":[......]
    }   
}

在傳統(tǒng)的寫法中晃痴,一般在Activity/Fragment中緩存page倘核,totalPage紧唱,pageSize去進行分頁請求隶校,根據(jù)請求結(jié)果刷新數(shù)據(jù)并判斷是否還有更多深胳;每一個分頁接口都要寫一遍舞终,假如把這段邏輯放到DataManager會怎么樣?我是這么寫的:

//定義回調(diào)接口
public interface ActionCallback<T> {
    void onSuccess(T data);

    void onFailure(String message, Throwable e);
}

分頁加載DataManager實現(xiàn)

public class PageLoadDataManager extends BaseDataManager {
    private static final int PAGE_COUNT = 20;

    private List<Data> mDataList = new ArrayList<>();
    private int currentPage = 0;
    private int totalPage = 0;

    public PageLoadDataManager() {
        // init something......
    }

    public void loadData(final boolean refresh, ActionListener<Boolean> listener) {
        if (refresh) {
            currentPage = 0;
        }
        currentPage++;
        RequestParams params = new RequestParams();
        params.put("page", currentPage);
        Request request = new Request(url, params);
        request.request(new RequestCallback(){
           @Override 
           public void onSuccess(JSONObject data) {
               if (refresh) {
                    mDataList.clear();
               }
               totalPage = response.optInt("total_page");
               // 返回數(shù)據(jù)添加到 mDataList ......
               if (listener != null) {
                   boolean hasMore = currentPage <= totalPage
                   listener.onSuccess(hasMore);
               }
           }
           @Override 
           public void onFailure(String message, Throwable e) {
               if (listener != null) {
                   listener.onFailure(message, e);
               }
           }
        });
    }
    public List<Data> getDataList() {
        return mDataList;
    }
}

Activity/Fragment初始化DataManager之后,只需要將數(shù)據(jù)源綁定到Adapter龙屉,loadData設(shè)置的回調(diào)告訴上層還有沒有更多數(shù)據(jù)转捕,UI層調(diào)用adapter.notifyDataSetChanged( )唆垃;至于數(shù)據(jù)從哪來辕万,分頁邏輯,根本不需要UI層管理醉途。UI層只需要通過loadData(refresh),告訴DataManager是否需要重新加載分頁殴穴,與下拉刷新的邏輯完美契合采幌。

當(dāng)然休傍,在此基礎(chǔ)上實現(xiàn)數(shù)據(jù)庫緩存讀寫尊残,也毫無壓力淤堵。DataManager也很容易實現(xiàn)對某一數(shù)據(jù)的多個接口的統(tǒng)一管理拐邪,通過單例模式或者其他管理方法,將數(shù)據(jù)配發(fā)給多個頁面汹胃。

  • 優(yōu)點:大幅減輕Activity/Fragment的壓力着饥,實現(xiàn)數(shù)據(jù)統(tǒng)一管理惰赋,DataManager層成為了一個UI無關(guān)的AppSDK層.
  • 缺點:需要添加嵌套回調(diào)赁濒,這個問題在引入RxJava之后被完美處理。

其實到了這一步挪拟,已經(jīng)能滿足大多數(shù)幾萬行代碼規(guī)模中小App的框架需求了玉组,而且分層架構(gòu)統(tǒng)一處理數(shù)據(jù)以及代碼復(fù)用度高的特點惯雳,使得項目中按照框架思路實現(xiàn)業(yè)務(wù)成為最快速可靠的開發(fā)方法。我認為一個優(yōu)秀的框架,很重要的特性就是方便業(yè)務(wù)開發(fā)而不是給開發(fā)找麻煩鸵钝,比如在分層設(shè)計過后恩商,就算開發(fā)時間再緊張必逆,依托分層框架依然是最快最保險的開發(fā)方法粟矿,假如某個接口直接在UI中寫了损拢,就意味著數(shù)據(jù)管理層提供的一切便利都無法直接使用福压,而且假如其他UI用到這個接口荆姆,還得再復(fù)制粘貼一遍改來改去胆筒,相反,依托框架决乎,網(wǎng)絡(luò)調(diào)用只實現(xiàn)一遍构诚,上層即可重復(fù)使用這一業(yè)務(wù)接口(比較典型的:關(guān)注范嘱、收藏等)丑蛤,即便如此,項目規(guī)模進一步往上之后碌补,DataManager厦章,Activity/Fragment的壓力仍然會增大袜啃,更高的測試需求群发,要求進一步分離Activity/Fragment的代碼熟妓。這時候就可以看看MVP和MVVM了滑蚯。

3. MVP架構(gòu)

MVC的C是即持有具體Model告材,又持有具體View斥赋,所以C很臃腫,分層架構(gòu)就算抽出了DataManager闷堡,實質(zhì)上仍然是一個MVC架構(gòu)杠览,而MVP和MVVM則是C持有具體View這個問題做了點文章管钳,其中MVP就是將大量的View <-> Model 交互剝離出來交由Presenter,Presenter持有抽象的View才漆。

在去年寫這個回答的時候牛曹,我曾經(jīng)寫過這么一段:看上去很美好,但是網(wǎng)上很多博客的那種Demo寫法我在嘗試應(yīng)用中發(fā)現(xiàn)并不實用醇滥,就是抽象出很多View接口黎比,然后建立Presenter類來作為Presenter,這樣做寫些簡單的列表獲取鸳玩,登錄之類看起來很漂亮焰手,好像做到了代碼分離,但是業(yè)務(wù)場景一復(fù)雜就有點蛋疼

那個時候我還僅僅只是嘗試怀喉,不實用是一個很感性的認識,也沒有多說船响,那時候是在做一個商城應(yīng)用躬拢,使用MVP編寫諸如購物車之類復(fù)雜場景的時候遇到了很大的困難,以至于讓我懷疑我是不是在用MVP給自己找麻煩,寫登錄這些還好,寫到購物車的時候我就開始懷疑人生了。一個ICartView,我要寫多少接口回季?購物車查刪改鼻忠、優(yōu)惠券滿減查、湊單、價格計算埋涧、運費……二十個接口少不了吧醇坝?那么這個抽象的View除了給CartActivity用宋距,還有其它什么卵用嗎壶唤?假如我寫成ICartView,IBonusView岛啸,IXXXView……可是有的界面并不需要刪改購物車列表啊,難道我還要再細分批幌?然后讓Activity實現(xiàn)一堆接口截粗?搞成這個樣子珊蟀,假如哪天需求變了怎么辦……Presenter聽起來很吊描扯,主導(dǎo)者啊恩够,但是沒有Activity和Fragment的資源啊,我要怎么才能讓它主導(dǎo)?需要獲取系統(tǒng)的一些信息(需要Context)的時候怎么辦?不持有Context難道再開接口嗎?寫這么多接口票从,接口實現(xiàn),Presenter兜看,多寫了幾百行代碼n個類碗殷,就為了把1~200行代碼從Activity移出去仿粹?還是放棄吧……

后來Google出了TODO-MVP,但是發(fā)現(xiàn)跟上面那種Demo寫法一樣很麻煩,我也沒有實際運用绘搞。后來反編譯了某個大型App,發(fā)現(xiàn)其正好是MVP架構(gòu)啄栓,于是仔細看了一下代碼,就如同我最開始的想法诈嘿,一個IXXXView有多少功能就寫多少接口。再看看Presenter的實現(xiàn)蛮瞄,我忽然就明白我為什么會感覺不實用了:

任何想要構(gòu)建一個其他什么東西取代Activity/Fragment地位的嘗試都是自找麻煩

MVP正是一個典型。既然MVP把Activity/Fragment抽象為View酣倾,那么就意味著當(dāng)它作為一個抽象View去使用的時候墅垮,生命周期惕医,Context這些極其重要的資源Presenter是看不到的,但是這些東西是不可能不使用的算色。為了能讓Presenter使用到這些抬伺,Presenter就必須持有Context,綁定Activity灾梦、Fragment的生命周期峡钓,就算如此,在一些需要確定使用Activity若河、Fragment的場合能岩,仍需要使用強制轉(zhuǎn)型。正因為Presenter這個“主導(dǎo)”萧福,導(dǎo)致Presenter和Activity/Fragment高度綁定拉鹃,Presenter和IXXXView,沒有什么復(fù)用性鲫忍。這是我對目前Android MVP的一點看法膏燕,如果有小伙伴有比較好的實踐經(jīng)驗,可以在評論告訴我悟民。

4. MVVM(Model-View-ViewModel)架構(gòu)

在我研究MVP的時間點坝辫,MVVM也是一個很火的概念,基于data-binding框架的demo也很多射亏,但是我看過之后立刻否決了這個方案近忙,大部分應(yīng)用在從接口獲取數(shù)據(jù)后都會進行數(shù)據(jù)變換竭业,哪怕拿到一個圖片URL都會在Java層添加后綴獲取縮略圖,有的要根據(jù)數(shù)據(jù)源控制View大小及舍,顯隱永品,XML能做的事情太少了,如果將Model綁定到XML击纬,大規(guī)模應(yīng)用將會面臨多少坑……

MVVM相比于MVP鼎姐,最重要的一個概念就是“數(shù)據(jù)綁定”!Presenter還持有抽象的View更振,ViewModel連這個都不需要炕桨,View通過ViewModel訂閱其所需的數(shù)據(jù)源,ViewModel向View提供改變數(shù)據(jù)的接口肯腕,當(dāng)View的操作引起數(shù)據(jù)改變或者數(shù)據(jù)源發(fā)生改變時献宫,ViewModel通過訂閱告知View,View進行視圖更新实撒。這就是MVVM吸引人的地方姊途,ViewModel只提供數(shù)據(jù)訂閱和數(shù)據(jù)接口,做到了與UI分離知态,ViewModel體量比Presenter小捷兰,復(fù)用性要比Presenter強太多,而且基于分層架構(gòu)可以做到小幅修改就能實現(xiàn)负敏。唯一的痛點在于:如何實現(xiàn)數(shù)據(jù)綁定贡茅?

之前提到的data-binding,并不是那么如意其做,而這次Google I/O 2017放出的android-architecture-components則很好的解決了這個問題顶考。

  • ViewModel組件
    規(guī)范了ViewModel的所處地位,生命周期妖泄,生成方式驹沿,以及一個Activity下多個Fragment共享ViewModel數(shù)據(jù)的問題
  • LiveData組件
    提供了在Java層面View訂閱ViewModel數(shù)據(jù)源的實現(xiàn)方案,很輕量蹈胡。

ViewModel的引入能夠很好應(yīng)對Activity銷毀重建時大規(guī)模數(shù)據(jù)的恢復(fù)問題渊季,以及多個界面依賴一個接口返回數(shù)據(jù)的場景,在這兩個組件的規(guī)范下實現(xiàn)MVVM架構(gòu)會十分容易审残,而且十分有意義梭域。

由于我已經(jīng)在項目中大規(guī)模使用了RxJava斑举,因此數(shù)據(jù)綁定我是采用RxJava方案實現(xiàn)的搅轿。

關(guān)于使用 android-architecture-components 組件實現(xiàn)MVVM的方案可以參考:
googlesamples/android-architecture-components
關(guān)于 新型MVVM結(jié)構(gòu)的思路,推薦這三篇文章
Android官方架構(gòu)組件指南
Android官方架構(gòu)組件介紹之ViewModel
Android官方架構(gòu)組件介紹之LiveData

5. 組件化和插件化

這兩年來這兩個概念很火富玷,但需要注意的一點是璧坟,這兩個概念和上面的東西并不是一個層級的既穆,組件化和插件化是比上面說的那一堆亂七八糟更上層的東西,是針對整個大工程下的若干小模塊來說的雀鹃,而這些小模塊怎樣搭建幻工,則還是上面那些內(nèi)容:)

6. 一點總結(jié)

一般來說我們做App,比如小外包黎茎,其實是用不到MVP囊颅,MVVM這樣的架構(gòu)的,一個分層架構(gòu)就足以讓我們快速高效的開發(fā)出App傅瞻,選用什么框架踢代,不僅要看你的應(yīng)用類型,也要看你的應(yīng)用規(guī)模嗅骄,在分層架構(gòu)的基礎(chǔ)上胳挎,只要接口實現(xiàn)的足夠好,代碼夠規(guī)范溺森,切換到MVVM這樣的架構(gòu)也不是什么很難的事情慕爬。

如果你有現(xiàn)成成熟的框架那無需多言,但如果你的應(yīng)用只有幾千行代碼屏积,為了追求MVVM医窿,寫了十幾個類,踩了若干坑炊林,只為了把一個Activity中的幾十行代碼抽到ViewModel里面留搔,豈不是南轅北轍?

最后分享一個我自己的代碼庫和基礎(chǔ)框架工程铛铁,有沒集成RxJava的基礎(chǔ)分支隔显、集成了RxJava的分層框架分支,還有一個使用android-arch-components的mvvm-rx分支饵逐,目前網(wǎng)絡(luò)調(diào)用這塊還不怎么完善括眠,后面會逐步完善演示示例,希望能幫助到大家倍权。

ShonLin/QuickDevFramework

最后鳴謝:知乎 0x8421bcd

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掷豺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子薄声,更是在濱河造成了極大的恐慌当船,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件默辨,死亡現(xiàn)場離奇詭異德频,居然都是意外死亡,警方通過查閱死者的電腦和手機缩幸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門壹置,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竞思,“玉大人,你說我怎么就攤上這事钞护「桥纾” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵难咕,是天一觀的道長课梳。 經(jīng)常有香客問我,道長余佃,這世上最難降的妖魔是什么惦界? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮咙冗,結(jié)果婚禮上沾歪,老公的妹妹穿的比我還像新娘。我一直安慰自己雾消,他們只是感情好灾搏,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著立润,像睡著了一般狂窑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桑腮,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天泉哈,我揣著相機與錄音,去河邊找鬼破讨。 笑死丛晦,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的提陶。 我是一名探鬼主播烫沙,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼隙笆!你這毒婦竟也來了锌蓄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撑柔,失蹤者是張志新(化名)和其女友劉穎瘸爽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铅忿,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡剪决,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昼捍。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肢扯,靈堂內(nèi)的尸體忽然破棺而出妒茬,到底是詐尸還是另有隱情,我是刑警寧澤蔚晨,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布乍钻,位于F島的核電站,受9級特大地震影響铭腕,放射性物質(zhì)發(fā)生泄漏银择。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一累舷、第九天 我趴在偏房一處隱蔽的房頂上張望浩考。 院中可真熱鬧,春花似錦被盈、人聲如沸析孽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袜瞬。三九已至,卻和暖如春身堡,著一層夾襖步出監(jiān)牢的瞬間邓尤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工贴谎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留汞扎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓擅这,卻偏偏與公主長得像佩捞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蕾哟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359