本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
前言
android架構(gòu)可能是論壇討論最多的話題了螟够,mvc mvp和mvvm不絕于耳,后面又有模塊化和插件化嗽冒。對(duì)此,關(guān)于哪種架構(gòu)更好的爭論從未停止。
我的觀點(diǎn):脫離實(shí)際項(xiàng)目比較這些模式優(yōu)劣毫無意義哗伯,各種模式都有優(yōu)點(diǎn)和缺點(diǎn)讥蔽,沒有好壞之分涣易。越高級(jí)的架構(gòu)實(shí)現(xiàn)起來越復(fù)雜,需要更多的學(xué)習(xí)成本更多的人力冶伞,所以說技術(shù)選型關(guān)鍵是在你自己項(xiàng)目的特點(diǎn)新症,團(tuán)隊(duì)的水平,資源的配備响禽,開發(fā)時(shí)間的限制徒爹,這些才是重點(diǎn)!但是不少團(tuán)隊(duì)本末倒置芋类,把mvvm往自己的項(xiàng)目硬套隆嗅。
下面我從兩大塊講下我理解的Android架構(gòu):代碼層面,主要是MVC和MVP的理解侯繁。項(xiàng)目層面胖喳,主要是怎么搭建整個(gè)項(xiàng)目,怎么劃分模塊贮竟。
通俗理解MVC和MVP
先上結(jié)論:
- MVC:Model-View-Controller丽焊,經(jīng)典模式,很容易理解咕别,主要缺點(diǎn)有兩個(gè):
- View對(duì)Model的依賴技健,會(huì)導(dǎo)致View也包含了業(yè)務(wù)邏輯;
- Controller會(huì)變得很厚很復(fù)雜惰拱。
- MVP:Model-View-Presenter雌贱,MVC的一個(gè)演變模式,將Controller換成了Presenter,主要為了解決上述第一個(gè)缺點(diǎn)帽芽,將View和Model解耦删掀,不過第二個(gè)缺點(diǎn)依然沒有解決。
- MVVM:Model-View-ViewModel导街,是對(duì)MVP的一個(gè)優(yōu)化模式披泪,采用了雙向綁定:View的變動(dòng),自動(dòng)反映在ViewModel搬瑰,反之亦然款票。
MVC
簡單的說:我們平時(shí)寫的Demo都是MVC,controller就是我們的activity泽论,model(數(shù)據(jù)提供者)就是讀取數(shù)據(jù)庫艾少,網(wǎng)絡(luò)請(qǐng)求這些我們一般有專門的類處理,View一般用自定義控件翼悴。
但這一切缚够,只是看起來很美。
想象實(shí)際開發(fā)中鹦赎,我們的activity代碼其實(shí)是越來越多谍椅,model和controller根本沒有分離,控件也需要關(guān)系數(shù)據(jù)和業(yè)務(wù)古话。
所以說雏吭,MVC的真實(shí)存在是MC(V),Model和Controller根本沒辦法分開陪踩,并且數(shù)據(jù)和View嚴(yán)重耦合杖们。這就是它的問題。舉個(gè)簡單例子 :獲取天氣數(shù)據(jù)展示在界面上
- Model層
public interface WeatherModel {
void getWeather(String cityNumber, OnWeatherListener listener);
}
................
public class WeatherModelImpl implements WeatherModel {
@Override
public void getWeather(String cityNumber, final OnWeatherListener listener) {
/*數(shù)據(jù)層操作*/
VolleyRequest.newInstance().newGsonRequest(http://www.weather.com.cn/data/sk/ + cityNumber + .html,
Weather.class, new Response.Listener<weather>() {
@Override
public void onResponse(Weather weather) {
if (weather != null) {
listener.onSuccess(weather);
} else {
listener.onError();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
listener.onError();
}
});
}
}
Controllor(View)層
public class MainActivity extends ActionBarActivity implements OnWeatherListener, View.OnClickListener {
private WeatherModel weatherModel;
private EditText cityNOInput;
private TextView city;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
weatherModel = new WeatherModelImpl();
initView();
}
//初始化View
private void initView() {
cityNOInput = findView(R.id.et_city_no);
city = findView(R.id.tv_city);
...
findView(R.id.btn_go).setOnClickListener(this);
}
//顯示結(jié)果
public void displayResult(Weather weather) {
WeatherInfo weatherInfo = weather.getWeatherinfo();
city.setText(weatherInfo.getCity());
...
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_go:
weatherModel.getWeather(cityNOInput.getText().toString().trim(), this);
break;
}
}
@Override
public void onSuccess(Weather weather) {
displayResult(weather);
}
@Override
public void onError() {
Toast.makeText(this, 獲取天氣信息失敗, Toast.LENGTH_SHORT).show();
}
private T findView(int id) {
return (T) findViewById(id);
}
}
簡單分析下這個(gè)例子:
1肩狂、activity里面的控件必須關(guān)心業(yè)務(wù)和數(shù)據(jù)摘完,才能知道自己怎么展示。換句話說婚温,我們很難讓兩個(gè)人在不互相溝通的情況下描焰,一人負(fù)責(zé)獲取數(shù)據(jù),一人負(fù)責(zé)展示UI栅螟,然后完成這個(gè)功能荆秦。
2、所以的邏輯都在activity里面力图。
完美的體現(xiàn)了MVC的兩大缺點(diǎn)步绸,下面看看MVP怎么解決第一個(gè)缺點(diǎn)的
MVP
看上圖可以看出,從MVC中View被拆成了Presenter和View吃媒,真正實(shí)現(xiàn)了邏輯處理和View的分離瓤介。下面寫一個(gè)實(shí)例:模擬一個(gè)登錄界面吕喘,輸入用戶名和密碼,可以登錄以及清除密碼
- Model層
/**
定義業(yè)務(wù)接口
*/
public interface IUserBiz
{
public void login(String username, String password, OnLoginListener loginListener);
}
/**
結(jié)果回調(diào)接口
*/
public interface OnLoginListener
{
void loginSuccess(User user);
void loginFailed();
}
/**
具體Model的實(shí)現(xiàn)
*/
public class UserBiz implements IUserBiz
{
@Override
public void login(final String username, final String password, final OnLoginListener loginListener)
{
//模擬子線程耗時(shí)操作
new Thread()
{
@Override
public void run()
{
try
{
Thread.sleep(2000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
//模擬登錄成功
if ("zhy".equals(username) && "123".equals(password))
{
User user = new User();
user.setUsername(username);
user.setPassword(password);
loginListener.loginSuccess(user);
} else
{
loginListener.loginFailed();
}
}
}.start();
}
}
- View
上面說到View層是以接口的形式定義刑桑,我們不關(guān)心數(shù)據(jù)氯质,不關(guān)心邏輯處理!只關(guān)心和用戶的交互祠斧,那么這個(gè)登錄界面應(yīng)該有的操作就是(把這個(gè)界面想成一個(gè)容器闻察,有輸入和輸出):
獲取用戶名,獲取密碼琢锋,現(xiàn)實(shí)進(jìn)度條辕漂,隱藏進(jìn)度條,跳轉(zhuǎn)到其他界面吴超,展示失敗dialog钉嘹,清除用戶名,清除密碼鲸阻。接下來定義接口:
public interface IUserLoginView
{
String getUserName();
String getPassword();
void clearUserName();
void clearPassword();
void showLoading();
void hideLoading();
void toMainActivity(User user);
void showFailedError();
}
然后Activity實(shí)現(xiàn)這個(gè)這個(gè)接口:
public class UserLoginActivity extends ActionBarActivity implements IUserLoginView
{
private EditText mEtUsername, mEtPassword;
private Button mBtnLogin, mBtnClear;
private ProgressBar mPbLoading;
private UserLoginPresenter mUserLoginPresenter = new UserLoginPresenter(this);
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_login);
initViews();
}
private void initViews()
{
mEtUsername = (EditText) findViewById(R.id.id_et_username);
mEtPassword = (EditText) findViewById(R.id.id_et_password);
mBtnClear = (Button) findViewById(R.id.id_btn_clear);
mBtnLogin = (Button) findViewById(R.id.id_btn_login);
mPbLoading = (ProgressBar) findViewById(R.id.id_pb_loading);
mBtnLogin.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
mUserLoginPresenter.login();
}
});
mBtnClear.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
mUserLoginPresenter.clear();
}
});
}
@Override
public String getUserName()
{
return mEtUsername.getText().toString();
}
@Override
public String getPassword()
{
return mEtPassword.getText().toString();
}
@Override
public void clearUserName()
{
mEtUsername.setText("");
}
@Override
public void clearPassword()
{
mEtPassword.setText("");
}
@Override
public void showLoading()
{
mPbLoading.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading()
{
mPbLoading.setVisibility(View.GONE);
}
@Override
public void toMainActivity(User user)
{
Toast.makeText(this, user.getUsername() +
" login success , to MainActivity", Toast.LENGTH_SHORT).show();
}
@Override
public void showFailedError()
{
Toast.makeText(this,
"login failed", Toast.LENGTH_SHORT).show();
}
}
- Presenter
Presenter的作用就是從View層獲取用戶的輸入跋涣,傳遞到Model層進(jìn)行處理,然后回調(diào)給View層鸟悴,輸出給用戶仆潮!
public class UserLoginPresenter
{
private IUserBiz userBiz;
private IUserLoginView userLoginView;
private Handler mHandler = new Handler();
//Presenter必須要能拿到View和Model的實(shí)現(xiàn)類
public UserLoginPresenter(IUserLoginView userLoginView)
{
this.userLoginView = userLoginView;
this.userBiz = new UserBiz();
}
public void login()
{
userLoginView.showLoading();
userBiz.login(userLoginView.getUserName(), userLoginView.getPassword(), new OnLoginListener()
{
@Override
public void loginSuccess(final User user)
{
//需要在UI線程執(zhí)行
mHandler.post(new Runnable()
{
@Override
public void run()
{
userLoginView.toMainActivity(user);
userLoginView.hideLoading();
}
});
}
@Override
public void loginFailed()
{
//需要在UI線程執(zhí)行
mHandler.post(new Runnable()
{
@Override
public void run()
{
userLoginView.showFailedError();
userLoginView.hideLoading();
}
});
}
});
}
public void clear()
{
userLoginView.clearUserName();
userLoginView.clearPassword();
}
}
分析下這個(gè)例子:
1、我們有了IUserLoginView 這個(gè)接口(協(xié)議)遣臼,activity里面的控件根本不需要關(guān)心數(shù)據(jù),只要實(shí)現(xiàn)這個(gè)接口在每個(gè)方法中“按部就班”的展示UI就行了拾并。換句話說揍堰,我們讓兩個(gè)人一起開發(fā)這個(gè)功能,一人要處理數(shù)據(jù)并且制定接口(協(xié)議)嗅义,另一人直接用activity實(shí)現(xiàn)這個(gè)接口屏歹,閉著眼睛就可以在每個(gè)回調(diào)里展示UI,合作很愉快之碗。
2蝙眶、MVP成功解決了MVC的第一個(gè)缺點(diǎn),但是邏輯處理還是雜糅在Activity褪那。
MVC到MVP簡單說幽纷,就是增加了一個(gè)接口降低一層耦合。那么博敬,用樣的MVP到MVVM就是再加一個(gè)接口唄友浸。實(shí)際項(xiàng)目我建議用MVP模式,MVVM還是復(fù)雜了對(duì)于中小型項(xiàng)目有點(diǎn)過度設(shè)計(jì)偏窝,這里就不展開講收恢。
模塊化
上圖是一個(gè)項(xiàng)目常見的架構(gòu)方式
1武学、最底層是基礎(chǔ)庫,放置與業(yè)務(wù)無關(guān)的模塊:比如基礎(chǔ)網(wǎng)絡(luò)請(qǐng)求伦意,圖片壓縮等等火窒,可以按需分為邏輯模塊,通用UI模塊和第三方庫驮肉。(建議采用獨(dú)立的svn分支)
2熏矿、中間層是通用業(yè)務(wù)層,放置公司多個(gè)android項(xiàng)目的通用業(yè)務(wù)模塊(和業(yè)務(wù)相關(guān)的)缆八,比如登錄流程曲掰,文件上傳/下載等。
3奈辰、最上層就是應(yīng)用層了栏妖,比如公司有三個(gè)android項(xiàng)目:LbBoss,BV和BVHD。我們還可以針對(duì)相似的項(xiàng)目再抽取通用層(比如這里的BV和BV PAD版奖恰,通用層為BVCommon)吊趾。
新建一個(gè)app,我們往往有兩種模塊劃分方法:
-
按照類型劃分:
- 按照業(yè)務(wù)劃分:
每一個(gè)包都是一個(gè)業(yè)務(wù)模塊瑟啃,每個(gè)模塊下再按照類型來分论泛。 - 怎么選
我建議中小型的新項(xiàng)目按照類型比較好,因?yàn)殚_始代碼量不多按照業(yè)務(wù)來分不切實(shí)際蛹屿,一個(gè)包只放幾個(gè)文件屁奏?? 況且前期業(yè)務(wù)不穩(wěn)定错负,等到開發(fā)中期業(yè)務(wù)定型了坟瓢,再進(jìn)行重構(gòu)難度也不大。
上面講的模塊劃分既不屬于模塊化也不屬于插件化犹撒,僅僅是一個(gè)簡單package結(jié)構(gòu)不同而已,app還是一個(gè)app并沒有產(chǎn)生什么變化折联。通常講的模塊化,是指把業(yè)務(wù)劃分為不同的moduler(類型是library)识颊,每個(gè)moduler之間都不依賴诚镰,app(類型是application)只是一個(gè)空殼依賴所有的moduler。
每個(gè)紅色箭頭都是一個(gè)業(yè)務(wù)模塊祥款,紅色框是我們的app里面只包含簡單的業(yè)務(wù):自定義Application清笨,入口Activity,build.gradle編譯打包配置镰踏『睿看下項(xiàng)目的依賴關(guān)系:
這樣架構(gòu)后,帶來最大的不同就是:不同業(yè)務(wù)模塊完全分離奠伪,好處就是不同模塊的開發(fā)絕對(duì)不會(huì)互相耦合了跌帐,因?yàn)槟阍谀KA 根本訪問不到模塊B的API首懈。此時(shí)模塊間通信急需解決,Intent隱式跳轉(zhuǎn)可以處理部分Activity的跳轉(zhuǎn)谨敛,但真正的業(yè)務(wù)場景遠(yuǎn)不止兩個(gè)界面跳一跳究履。你之前封裝的業(yè)務(wù)通用方法,工具類脸狸,數(shù)據(jù)緩存現(xiàn)在其他模塊都拿不到了最仑,本本來可以復(fù)用的控件,fragment都不能共享炊甲,而這些都是和業(yè)務(wù)耦合沒辦法拿到底層基礎(chǔ)庫泥彤。
模塊間通信
針對(duì)上面問題有兩個(gè)解決辦法,根據(jù)自己項(xiàng)目實(shí)際情況卿啡,如果項(xiàng)目的前期搭建已經(jīng)很優(yōu)秀吟吝,有完善的基礎(chǔ)庫,不同模塊間的通信不是很多颈娜,可以自己實(shí)現(xiàn)剑逃。如果項(xiàng)目比較龐大,不同業(yè)務(wù)間頻繁調(diào)用建議使用阿里巴巴的開源庫官辽。
- 自己實(shí)現(xiàn)
1蛹磺、首先每個(gè)moduler有個(gè)目錄叫include,里面有三個(gè)類同仆,此處以一個(gè)bbs論壇模塊為例說明萤捆,
IBBSNotify:里面是一堆interface,作用是該模塊對(duì)外的回調(diào)俗批,只能被動(dòng)被觸發(fā)鳖轰。
IBBService:里面是一堆interface,作用是對(duì)外暴露的方法扶镀,讓別的模塊來主動(dòng)調(diào),比如enterBbsActivity
IBBSServiceImpl:很明顯是IBBService的實(shí)現(xiàn)焰轻,比如enterBbsActivity就是具體怎么跳轉(zhuǎn)到論壇界面臭觉,傳遞什么數(shù)據(jù)。
2辱志、每個(gè)模塊方法和回調(diào)都有了蝠筑,in和out都具備了,別的模塊怎么使用呢揩懒?就該app該上場了什乙,app不能只是一個(gè)殼里面要定義一個(gè)ModulerManager implements 所有模塊的對(duì)外interface,作為每個(gè)模塊的中轉(zhuǎn)站已球,A模塊告訴ModulerManager我想跳轉(zhuǎn)到論壇模塊臣镣,接著ModulerManager調(diào)用IBBService.enterBbsActivity辅愿,IBBSServiceImpl是IBBService的具體實(shí)現(xiàn)(多態(tài))然后調(diào)用IBBSServiceImpl.enterBbsActivity跳轉(zhuǎn)到BBS界面。
3忆某、通信是解決了点待,其實(shí)踩坑才剛剛開始:
a. 這里的app是我們新建的,那么之前項(xiàng)目的app模塊要降為library:
apply plugin: 'com.android.library'
殼app的build.gradle配置:
apply plugin: 'com.android.application'
性質(zhì)發(fā)生巨大變化弃舒。里面的自定義application癞埠,build.gradle,代碼混淆配置等全部移到app
b.R.java在Lib類型的moduler中不是final的聋呢,所有switch case語句全部替換成if else
c.一定要再建一個(gè)common模塊苗踪,放置通用數(shù)據(jù),緩存等
d.還有很多通用功能削锰,例如分享通铲,推送,盡量剝離業(yè)務(wù)放到common
e.其他與項(xiàng)目相關(guān)的細(xì)節(jié)
- 開源庫ARouter
專門用于接續(xù)模塊間通信喂窟,這里不講了使用起來很簡單测暗。
其他
插件化:
插件化其實(shí)最后發(fā)布的產(chǎn)品也是一個(gè)apk,只不過大小可以控制(可以隨意去掉某些模塊)磨澡,支持用戶動(dòng)態(tài)加載子apk碗啄。因此,插件化就是動(dòng)態(tài)加載apk稳摄。有人說我用intent隱式可以直接跳轉(zhuǎn)到另一個(gè)apk啊稚字,干嘛還要插件化。
其實(shí)是兩碼事厦酬,intent只是指定一個(gè)Activity跳過去胆描,后面的交互完成不受你控制,2個(gè)apk也是運(yùn)行在獨(dú)立的進(jìn)程數(shù)據(jù)無法共享仗阅。而插件化可以讓兩個(gè)apk運(yùn)行在一個(gè)進(jìn)程昌讲,可以完全像同一個(gè)apk一樣開發(fā)。不過减噪,我覺得插件化只適合需要多部門并行開發(fā)的那種短绸,比如支付寶這種超級(jí)app,一般的app開發(fā)除非特殊需要筹裕,否則用不到醋闭。
插件化也有成熟的框架拐辽,在此不詳細(xì)說了器躏。另外艺骂,每個(gè)人的習(xí)慣不一樣留瞳,組件化舔痪,模塊化在我看來差不多肾筐,沒必要糾結(jié)兩個(gè)名詞慷吊。