最近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使用妇萄。
這是比較基礎(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):
Activity和Fragment剝離了數(shù)據(jù)處理的責(zé)任后观腊,持有DataManager的引用梧油,負責(zé)獲取數(shù)據(jù)并展示,向DataManager傳遞數(shù)據(jù),絕不進行網(wǎng)絡(luò)請求和緩存讀寫迄委。
舉個栗子叙身,分頁加載。一般來說分頁加載接口返回的數(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)用這塊還不怎么完善括眠,后面會逐步完善演示示例,希望能幫助到大家倍权。
最后鳴謝:知乎 0x8421bcd