前言
一個好的軟件總是離不開好的架構,不管是前端后端。
在Android中,我已知的設計模式有:MVC,MVP、MVVM、Clean,其中各自的優(yōu)劣不再這里展開,有需要的自行Google。
這里探討一下MVP,在很多的文章中,都講很多的概念性的東西,時常把人講的云里霧里,對于剛接觸的人,就算是理解了,怎么實際應用都不知道。
因此本文就用最簡單最常見的來介紹MVP,其實架構是一種很活的東西,誰說你必須使用某種模式?誰規(guī)定代碼一定要這么寫才是對的?我認為只有在變化中能不斷適應的,才是王道。難道后來你會了另一種模式,就不能在已有的項目中應用了嗎?
我認為只要是你邏輯清晰,分層合理,你想怎么玩都行,甚至不用任何所謂的模式,注意:前提是分層一定要清晰,層與層之間的界限要清晰明了。
先不管概念,來一段簡單的代碼先
需求:用戶輸入賬號密碼,點擊登錄按鈕進行登錄。
代碼如下:注意,只是作為示范用。有所刪減,看得懂意圖就好。
activity_login.xml:
如圖,xml 的代碼就不貼了,很簡單。
LoginActivity.java:
public class LoginActivity
extends AppCompatActivity
{
private EditText etAccount;
private EditText etPwd;
@Override
protected void onCreate(
@Nullable
Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
etAccount = (EditText) findViewById(R.id.et_account);
etPwd = (EditText) findViewById(R.id.et_pwd);
}
// 響應登錄按鈕
public void onLogin(View view)
{
String account = etAccount.getText().toString();
String pwd = etPwd.getText().toString();
// TODO 這里省掉了空判斷
// 發(fā)起請求
RequestParams params = new RequestParams();
params.add("account", account);
params.add("pwd", pwd);
new AsyncHttpClient().get("url", params, new Login());
}
// 登錄請求回調
private class Login
extends AsyncHttpResponseHandler
{
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
{
if (responseBody != null)
{
LoginResponse response = JSON.parseObject(new String(responseBody),
LoginResponse.class);
if (response != null)
{
if (response.getStatus() == 0)
{
Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
// TODO 去到主界面之類的
// 然后結束掉登錄
finish();
}
else
{
Toast.makeText(LoginActivity.this, "登錄失敗," + response.getMsg(),
Toast.LENGTH_SHORT).show();
}
}
}
else
{
onFailure(statusCode, headers, null, null);
}
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
Throwable error)
{
Toast.makeText(LoginActivity.this, "登錄失敗,請檢查網(wǎng)絡", Toast.LENGTH_SHORT).show();
}
}
}
很簡單吧?就是輸入和發(fā)起登錄。
其中網(wǎng)絡庫使用的是:
android-async-http
JSON解析使用的是:
FastJSON Android版本
分析,以上代碼一共分為多少層?有什么缺陷?
-
UI層(View)
:界面的顯示,控件的綁定和操作,用戶的輸入和操作,都屬于UI層需要處理的。比如上述代碼中的findViewById,按鈕響應,Toast,跳轉到其他Activity等操作。在Android中,Activity、Fragment都屬于View。
-
業(yè)務邏輯層(Presenter)
:登錄請求的發(fā)起,結果的接收和處理,通知UI層界面更新,都屬于業(yè)務邏輯的范圍。比如上述代碼中的請求發(fā)起和JSON解析,判斷等。
-
數(shù)據(jù)層(Model)
:去服務器請求數(shù)據(jù),這里不只是云服務器的請求,數(shù)據(jù)庫,文件,智能設備,任何數(shù)據(jù)源,只要是增刪改查的,都屬于數(shù)據(jù)層的工作。比如上述代碼的網(wǎng)絡庫異步請求。
從分析來看,上述一個簡單的需求實際上有三個層
的存在,而卻全部寫在View中,對于新手來說,這樣類似的代碼是再正常不過了。
一般來說,簡單的需求,項目小,這樣寫也不會造成什么問題的,但是一旦項目越來越大,并且需求改動也越來越多的時候,就成了一種災難了,比如無休止的復制和粘貼。
舉個例子:
現(xiàn)在項目中加入了啟動頁,要求在啟動頁判斷先前是否已有用戶登錄過,如果有,則取出賬號密碼進行登錄,登錄成功去到主界面,失敗則去到登錄頁;
如果沒有,直接跳轉到登錄頁。
再用上面的寫法,也就是加個啟動頁,然后復制登錄的那段代碼,再改改回調處理的,聽起來好像沒事,但不覺得重復了嗎?
使用MVP模式重寫
先看一張類圖
你肯定會說:什么?一個簡單的功能,居然需要這么多類文件,這不是更加煩瑣,工作量更加大了嗎?
別急,繼續(xù)看。下面我們就按上面分析的來寫。
LoginResponse.java :
JSON 解析需要的數(shù)據(jù)類
public class LoginResponse
{
private int status;
private String msg;
...省略掉 set/get
}
再來看兩個 Base 類:
BasePresenter.java:
/**
* 所有Presenter的父接口
*/
public interface BasePresenter
{
// TODO 在這里可以聲明一些Presenter的通用方法
}
BaseView.java:
/**
* 所有View的父接口
*/
public interface BaseView
{
// TODO 在這里可以聲明一些View的通用方法
}
不知道定義兩個 Base 是用來干嘛的,沒關系,再來思考關于這個登錄界面的兩個問題:
- 1.需要我們處理的
用戶操作
有哪些? - 2.
界面?顯示相關的
,需要我們做的有哪些?
針對以上問題,解答如下:
- 1.只有
登錄
需要我們處理,其他的諸如輸入賬號密碼,點擊按鈕這種操作?不需要我們做。至于賬號密碼的空判斷,已經(jīng)包含在登錄
這個操作里了。 - 2.點擊登錄按鈕后,需要顯示
進度條
,登錄成功
需要顯示成功或直接去到主頁面之類的,登錄失敗
需要隱藏進度條,?提示登錄失敗的原因之類的。
因此我們可以?把這些操作和顯示都歸類到一個地方,稱為契約類(Contract)
。
LoginContract.java:
/**
* 登錄契約類,聲明了View和Presenter該有的操作,方便管理
*/
public interface LoginContract
{
// 定義界面中所有的 UI 狀態(tài)
interface View
extends BaseView
{
void loginSuccess(); // 登錄成功
void loginFailure(String msg); // 登錄失敗
void showLoading(boolean isShowLoading); // 是否顯示加載中
}
// 定義了所有的用戶操作
interface Presenter
extends BasePresenter
{
void login(String account, String pwd); // 登錄
}
}
定義契約類的目的是方便管理,也能理清你的邏輯。
好了,?以上都是準備工作,實際的 Model、View、Presenter 相關的具體類還沒寫。繼續(xù)看。
Model:LoginRequest.java
public final class LoginRequest
{
// 單例
private LoginRequest()
{
}
private static class SingletonHolder
{
private static final LoginRequest SINGLETON = new LoginRequest();
}
public static LoginRequest getInstance()
{
return SingletonHolder.SINGLETON;
}
public void login(String account, String pwd, final LoginCallback callback)
{
// 發(fā)起請求
RequestParams params = new RequestParams();
params.add("account", account);
params.add("pwd", pwd);
new AsyncHttpClient().get("url", params, new AsyncHttpResponseHandler()
{
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
{
callback.onSuccess(statusCode, responseBody);
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
Throwable error)
{
callback.onFailure(statusCode, responseBody, error);
}
});
}
// 對外暴露的接口
public interface LoginCallback
{
void onFailure(int statusCode, byte[] responseBody, Throwable error);
void onSuccess(int statusCode, byte[] responseBody);
}
}
Model 類不負責邏輯的處理,只是負責增刪改查
,以及必要的保存住自己的狀態(tài)
,比如你這個 Model 表示一個智能開關設備,那么開關的狀態(tài)你得保存起來,以便?狀態(tài)改變的時候發(fā)出通知,以及外面的人來你這拿狀態(tài)的時候,你得給人家正確的狀態(tài)。
這里的 Model 表示登錄,login 方法被調用后
,將登錄結果通過 LoginCallback
回傳給調用者就完成了職責。
再來看 View。
View:?LoginActivity.java:
public class LoginActivity
extends AppCompatActivity
implements LoginContract.View // 實現(xiàn)了?契約類中的接口
{
private EditText etAccount;
private EditText etPwd;
private LoginContract.Presenter loginPresenter;
@Override
protected void onCreate(
@Nullable
Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 創(chuàng)建Presenter,使View和Presenter,Presenter和Model關聯(lián)起來,這一步暫且忽略也可以,等看到 Presenter 了再回來看。
loginPresenter = new LoginPresenter(this, LoginRequest.getInstance());
etAccount = (EditText) findViewById(R.id.et_account);
etPwd = (EditText) findViewById(R.id.et_pwd);
}
public void onLogin(View view)
{
String account = etAccount.getText().toString();
String pwd = etPwd.getText().toString();
// 告知Presenter發(fā)起登錄
loginPresenter.login(account, pwd);
}
@Override
public void showLoading(boolean isShowLoading)
{
// 顯示和隱藏進度條
}
@Override
public void loginSuccess()
{
/*
比如取消進度條,進入到主頁面
*/
}
@Override
public void loginFailure(String msg)
{
/*
取消進度條,顯示登錄錯誤提示,比如密碼錯誤、賬號不存在之類的
*/
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}
可以看到,View 中沒有任何的邏輯處理和數(shù)據(jù)獲取,能做的??只是跟界面相關的操作,向?外界發(fā)出請求,以及?暴露給外界操作界面的方法。注意:View 中不能有任何的業(yè)務邏輯處理,只能有和 View 相關的操作。
可以看到,Model 和 View 是完全分開的,沒有任何的直接關聯(lián)。下一步,我們需要通過 Presenter 將他們關聯(lián)起來。
Presenter:LoginPresenter.java
public class LoginPresenter
implements LoginContract.Presenter
{
// Presenter ?持有 View 和 Model 的引用
private LoginContract.View loginView;
private LoginRequest loginRequest;
public LoginPresenter(LoginContract.View loginView, LoginRequest loginRequest)
{
this.loginView = loginView;
this.loginRequest = loginRequest;
}
@Override
public void login(String account, String pwd)
{
// 賬號密碼不對的話,直接失敗
if (TextUtils.isEmpty(account.trim()) || TextUtils.isEmpty(pwd))
{
loginView.showLoading(false);
loginView.loginFailure("賬號密碼不對");
return;
}
loginView.showLoading(true);
loginRequest.login(account, pwd, new LoginRequest.LoginCallback()
{
@Override
public void onFailure(int statusCode, byte[] responseBody, Throwable error)
{
loginView.loginFailure("登錄錯誤的提示信息");
}
@Override
public void onSuccess(int statusCode, byte[] responseBody)
{
if (responseBody != null)
{
LoginResponse response = JSON.parseObject(new String(responseBody),
LoginResponse.class);
if (response != null)
{
if (response.getStatus() == 0)
{
loginView.loginSuccess();
}
else
{
loginView.loginFailure("登錄錯誤的提示信息");
}
}
else
{
loginView.loginFailure("登錄錯誤的提示信息");
}
}
else
{
loginView.loginFailure("登錄錯誤的提示信息");
}
}
});
}
}
可以看到,所有的業(yè)務邏輯都在 Presenter 里面了。
看看以上的代碼是不是符合下面這張圖:
Model 和 View 是完全分離的,以上通過小實例目的是為了讓大家理解并用起來,更復雜徹底的 MVP ,可以查看 Google 的官方 Sample:
googlesamples/android-architecture
還有一個開源項目:
android10/Android-CleanArchitecture
MVP 的好處
Model 只有一個,View 只有一個,而 Presenter 可以有多個,但是一個 View 至少對應一個 Presenter,還是那句話,架構是很靈活的,你都把 Model 和 View 分開了,低耦合已經(jīng)實現(xiàn)了,怎么關聯(lián)他們,你看著辦咯。
設計圖出來了,接口還沒好,你可以專注先寫 View,完全不用管數(shù)據(jù)。設計圖沒好,接口好了,你可以先寫 Model,測試接口是否正常,完全不用管 View 是如何設計的。等到都設計好了,?你再把 Model 和 View 關聯(lián)起來專注寫邏輯。是不是覺得無比的清爽?
非常適合于大型的項目,但是要避免過度設計和正確的抽象。
MVP 的壞處
- 類爆炸
結語:
本文的目的是讓沒有玩過 MVP 的設計快速入門的,?理解了以上內容,?進階的內容可自己 Google,很多這方面的資料。