文:https://www.jianshu.com/p/ce26e7960926
最近App項目(MVC架構)越做越大,協同開發效率較低,維護困難,所以產生了調整架構的想法,在 簡書、csdn、知乎上看了不少文章,感覺知乎用戶 0x8421bcd 對於“Android項目開發如何設計整體架構?”的回答頗為精彩,在此引用,鞠躬感謝!
0. 前言
想要設計App的整體框架,首先要清楚我們做的是什么。一般我們與網絡交互數據的方式有兩種:主動請求(http)和長連接推送。
結合網絡交互數據的方式來說一下我們開發的App的類型和特點:
- 數據展示類型的App
特點是頁面多,需要頻繁調用后端接口進行數據交互,以http請求為主;推送模塊,IM類型App的IM核心功能以長連接為主,比較看重電量、流量消耗。 - 手機助手類App
主要着眼於系統API的調用,達到輔助管理系統的目的,網絡調用的方式以http為主。 - 游戲
一般分為游戲引擎和業務邏輯,業務腳本化編寫,網絡以長連接為主,http為輔。
一般我們做的App都是類型1,簡要來說這類app的主要工作就是把服務端的數據拉下來給用戶展示,把用戶在客戶端修改的數據上傳給服務端處理,所以這類App的網絡調用相當頻繁,而且需要考慮到網絡差、沒網絡等情況下App能夠正常運行。
成熟的商業應用的網絡調用一般是如下流程:UI發起請求 -> 檢查緩存 -> 調用網絡模塊 -> 解析返回JSON / 統一處理異常 -> JSON對象映射為Java對象 -> 緩存 -> UI獲取數據並展示,這之中可以看到很明顯職責划分,即:數據獲取;數據管理;數據展示。確定了職責,就可以進入正題了。
1. MVC架構
Android最原生也是最基礎的架構,可以理解為MVC(Model-View-Controller),Controller即是Activity和Fragment,但是這兩者掌握了Android系統中絕大多數的資源,並且在內部直接控制View,因此MVC架構一般是以Activity和Fragment為核心,將網絡模塊,數據庫管理模塊,文件管理模塊,常用工具類等分離成若干工具類包,供Activity和Fragment使用。

這是比較基礎的Android項目架構,市面上大部分App都是這種造型。
- 優點
開發簡單,以頁面為導向;如果構建水平可以,項目就已經基本實現模塊化,基於Activity、Fragment這兩個上帝般的存在,很多事情直接就妥了,不用繞。 - 缺點
維護難,因為是以頁面為導向的,有些需要共用的業務邏輯就會很煩,don't repeat your self, 你要不要repeat ?不想repeat就要寫模塊,慢慢的項目就會多出一堆亂七八糟的小模塊。另一方面,測試很困難,因為所有的數據處理都在Activity和Fragment,假如現在想先用假數據顯示,就要直接改Activity和Fragment的數據控制邏輯。還有個最惱火的問題,那就是業務復雜起來后Activity和Fragment的代碼量激增,舉一個例子,電商App的購物車,如果只是管理一下購物車中的商品,無非就是查、刪、改調用,列表管理,300多行代碼應該就搞定了,假如現在加了個優惠券提示呢?光優惠券不夠,還有滿減,還有湊單,要計算運費。還要能領取優惠券…… 噢,忘了一般來說還有一個商品推薦,好了現在有兩個列表要管理了,你覺得CartActivity 2000行代碼能止住么?在上面這些缺點的描述中,可以看到一個很大的痛點在於:Activity和Fragment不應該管這么多數據處理邏輯。
2. 分層架構(在MVC的基礎上分層)
如果仔細看自己的項目,可以發現絕大多數數據處理的代碼是不需要使用Activity和Fragment持有的資源的(比如Context),而很多時候我們需要多個頁面共用一套數據和請求邏輯,很經典的例子是應用中的User對象,一般來說都是全局單例。這些全局的數據源寫多了,很容易就能想到將數據處理統一抽出來形成一層,向上層提供數據接口,而上層並不關心數據的來源(內存,緩存,網絡),因為不用從Activity和Fragment拿資源而且主要工作是數據處理,所以這一層是UI無關的,大幅提升了復用性,我把這一層稱為DataManager層。
這是我一個項目的包結構:

MVC-Hierarchy.png
Activity和Fragment剝離了數據處理的責任后,持有DataManager的引用,負責獲取數據並展示,向DataManager傳遞數據,絕不進行網絡請求和緩存讀寫。

舉個栗子,分頁加載。一般來說分頁加載接口返回的數據是這樣的:
{
"code":0, "message":"success", "data":{ "page":1, "totalPage":10, "pageSize":20, "total":200, "list":[......] } }
在傳統的寫法中,一般在Activity/Fragment中緩存page,totalPage,pageSize去進行分頁請求,根據請求結果刷新數據並判斷是否還有更多;每一個分頁接口都要寫一遍,假如把這段邏輯放到DataManager會怎么樣?我是這么寫的:
//定義回調接口 public interface ActionCallback<T> { void onSuccess(T data); void onFailure(String message, Throwable e); }
分頁加載DataManager實現
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"); // 返回數據添加到 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之后,只需要將數據源綁定到Adapter,loadData設置的回調告訴上層還有沒有更多數據,UI層調用adapter.notifyDataSetChanged( );至於數據從哪來,分頁邏輯,根本不需要UI層管理。UI層只需要通過loadData(refresh),告訴DataManager是否需要重新加載分頁,與下拉刷新的邏輯完美契合。
當然,在此基礎上實現數據庫緩存讀寫,也毫無壓力。DataManager也很容易實現對某一數據的多個接口的統一管理,通過單例模式或者其他管理方法,將數據配發給多個頁面。
- 優點:大幅減輕Activity/Fragment的壓力,實現數據統一管理,DataManager層成為了一個UI無關的AppSDK層.
- 缺點:需要添加嵌套回調,這個問題在引入RxJava之后被完美處理。
其實到了這一步,已經能滿足大多數幾萬行代碼規模中小App的框架需求了,而且分層架構統一處理數據以及代碼復用度高的特點,使得項目中按照框架思路實現業務成為最快速可靠的開發方法。我認為一個優秀的框架,很重要的特性就是方便業務開發而不是給開發找麻煩,比如在分層設計過后,就算開發時間再緊張,依托分層框架依然是最快最保險的開發方法,假如某個接口直接在UI中寫了,就意味着數據管理層提供的一切便利都無法直接使用,而且假如其他UI用到這個接口,還得再復制粘貼一遍改來改去,相反,依托框架,網絡調用只實現一遍,上層即可重復使用這一業務接口(比較典型的:關注、收藏等),即便如此,項目規模進一步往上之后,DataManager,Activity/Fragment的壓力仍然會增大,更高的測試需求,要求進一步分離Activity/Fragment的代碼。這時候就可以看看MVP和MVVM了。
3. MVP架構
MVC的C是即持有具體Model,又持有具體View,所以C很臃腫,分層架構就算抽出了DataManager,實質上仍然是一個MVC架構,而MVP和MVVM則是C持有具體View這個問題做了點文章,其中MVP就是將大量的View <-> Model 交互剝離出來交由Presenter,Presenter持有抽象的View。
在去年寫這個回答的時候,我曾經寫過這么一段:看上去很美好,但是網上很多博客的那種Demo寫法我在嘗試應用中發現並不實用,就是抽象出很多View接口,然后建立Presenter類來作為Presenter,這樣做寫些簡單的列表獲取,登錄之類看起來很漂亮,好像做到了代碼分離,但是業務場景一復雜就有點蛋疼
那個時候我還僅僅只是嘗試,不實用是一個很感性的認識,也沒有多說,那時候是在做一個商城應用,使用MVP編寫諸如購物車之類復雜場景的時候遇到了很大的困難,以至於讓我懷疑我是不是在用MVP給自己找麻煩,寫登錄這些還好,寫到購物車的時候我就開始懷疑人生了。一個ICartView,我要寫多少接口?購物車查刪改、優惠券滿減查、湊單、價格計算、運費……二十個接口少不了吧?那么這個抽象的View除了給CartActivity用,還有其它什么卵用嗎?假如我寫成ICartView,IBonusView,IXXXView……可是有的界面並不需要刪改購物車列表啊,難道我還要再細分?然后讓Activity實現一堆接口?搞成這個樣子,假如哪天需求變了怎么辦……Presenter聽起來很吊,主導者啊,但是沒有Activity和Fragment的資源啊,我要怎么才能讓它主導?需要獲取系統的一些信息(需要Context)的時候怎么辦?不持有Context難道再開接口嗎?寫這么多接口,接口實現,Presenter,多寫了幾百行代碼n個類,就為了把1~200行代碼從Activity移出去?還是放棄吧……
后來Google出了TODO-MVP,但是發現跟上面那種Demo寫法一樣很麻煩,我也沒有實際運用。后來反編譯了某個大型App,發現其正好是MVP架構,於是仔細看了一下代碼,就如同我最開始的想法,一個IXXXView有多少功能就寫多少接口。再看看Presenter的實現,我忽然就明白我為什么會感覺不實用了:
任何想要構建一個其他什么東西取代Activity/Fragment地位的嘗試都是自找麻煩
MVP正是一個典型。既然MVP把Activity/Fragment抽象為View,那么就意味着當它作為一個抽象View去使用的時候,生命周期,Context這些極其重要的資源Presenter是看不到的,但是這些東西是不可能不使用的。為了能讓Presenter使用到這些,Presenter就必須持有Context,綁定Activity、Fragment的生命周期,就算如此,在一些需要確定使用Activity、Fragment的場合,仍需要使用強制轉型。正因為Presenter這個“主導”,導致Presenter和Activity/Fragment高度綁定,Presenter和IXXXView,沒有什么復用性。這是我對目前Android MVP的一點看法,如果有小伙伴有比較好的實踐經驗,可以在評論告訴我。
4. MVVM(Model-View-ViewModel)架構
在我研究MVP的時間點,MVVM也是一個很火的概念,基於data-binding框架的demo也很多,但是我看過之后立刻否決了這個方案,大部分應用在從接口獲取數據后都會進行數據變換,哪怕拿到一個圖片URL都會在Java層添加后綴獲取縮略圖,有的要根據數據源控制View大小,顯隱,XML能做的事情太少了,如果將Model綁定到XML,大規模應用將會面臨多少坑……
MVVM相比於MVP,最重要的一個概念就是“數據綁定”!Presenter還持有抽象的View,ViewModel連這個都不需要,View通過ViewModel訂閱其所需的數據源,ViewModel向View提供改變數據的接口,當View的操作引起數據改變或者數據源發生改變時,ViewModel通過訂閱告知View,View進行視圖更新。這就是MVVM吸引人的地方,ViewModel只提供數據訂閱和數據接口,做到了與UI分離,ViewModel體量比Presenter小,復用性要比Presenter強太多,而且基於分層架構可以做到小幅修改就能實現。唯一的痛點在於:如何實現數據綁定?
android-architecture-components
之前提到的data-binding,並不是那么如意,而這次Google I/O 2017放出的android-architecture-components則很好的解決了這個問題。
- ViewModel組件
規范了ViewModel的所處地位,生命周期,生成方式,以及一個Activity下多個Fragment共享ViewModel數據的問題 - LiveData組件
提供了在Java層面View訂閱ViewModel數據源的實現方案,很輕量。
ViewModel的引入能夠很好應對Activity銷毀重建時大規模數據的恢復問題,以及多個界面依賴一個接口返回數據的場景,在這兩個組件的規范下實現MVVM架構會十分容易,而且十分有意義。
由於我已經在項目中大規模使用了RxJava,因此數據綁定我是采用RxJava方案實現的。
關於使用 android-architecture-components 組件實現MVVM的方案可以參考:
googlesamples/android-architecture-components
關於 新型MVVM結構的思路,推薦這三篇文章
Android官方架構組件指南
Android官方架構組件介紹之ViewModel
Android官方架構組件介紹之LiveData
5. 組件化和插件化
這兩年來這兩個概念很火,但需要注意的一點是,這兩個概念和上面的東西並不是一個層級的,組件化和插件化是比上面說的那一堆亂七八糟更上層的東西,是針對整個大工程下的若干小模塊來說的,而這些小模塊怎樣搭建,則還是上面那些內容:)
6. 一點總結
一般來說我們做App,比如小外包,其實是用不到MVP,MVVM這樣的架構的,一個分層架構就足以讓我們快速高效的開發出App,選用什么框架,不僅要看你的應用類型,也要看你的應用規模,在分層架構的基礎上,只要接口實現的足夠好,代碼夠規范,切換到MVVM這樣的架構也不是什么很難的事情。
如果你有現成成熟的框架那無需多言,但如果你的應用只有幾千行代碼,為了追求MVVM,寫了十幾個類,踩了若干坑,只為了把一個Activity中的幾十行代碼抽到ViewModel里面,豈不是南轅北轍?
最后分享一個我自己的代碼庫和基礎框架工程,有沒集成RxJava的基礎分支、集成了RxJava的分層框架分支,還有一個使用android-arch-components的mvvm-rx分支,目前網絡調用這塊還不怎么完善,后面會逐步完善演示示例,希望能幫助到大家。
最后鳴謝:知乎 0x8421bcd