最简单的 MVP 理解
前言
一个好的软件总是离不开好的架构,不管是前端后端。
在Android中,我已知的设计模式有:MVC,MVP、MVVM、Clean,其中各自的优劣不再这里展开,有需要的自行Google。
这里探讨一下MVP,在很多的文章中,都讲很多的概念性的东西,时常把人讲的云里雾里,对于刚接触的人,就算是理解了,怎么实际应用都不知道。
因此本文就用最简单最常见的来介绍MVP,其实架构是一种很活的东西,谁说你必须使用某种模式?谁规定代码一定要这么写才是对的?我认为只有在变化中能不断适应的,才是王道。难道后来你会了另一种模式,就不能在已有的项目中应用了吗?
我认为只要是你逻辑清晰,分层合理,你想怎么玩都行,甚至不用任何所谓的模式,注意:前提是分层一定要清晰,层与层之间的界限要清晰明了。
先不管概念,来一段简单的代码先
需求:用户输入账号密码,点击登录按钮进行登录。
代码如下:注意,只是作为示范用。有所删减,看得懂意图就好。
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 这里省掉了空判断
// 发起请求
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, "登录失败,请检查网络", Toast.LENGTH_SHORT).show();
}
}
}
很简单吧?就是输入和发起登录。
其中网络库使用的是:
android-async-http
JSON解析使用的是:
FastJSON Android版本
分析,以上代码一共分为多少层?有什么缺陷?
-
UI层(View)
:界面的显示,控件的绑定和操作,用户的输入和操作,都属于UI层需要处理的。比如上述代码中的findViewById,按钮响应,Toast,跳转到其他Activity等操作。在Android中,Activity、Fragment都属于View。
-
业务逻辑层(Presenter)
:登录请求的发起,结果的接收和处理,通知UI层界面更新,都属于业务逻辑的范围。比如上述代码中的请求发起和JSON解析,判断等。
-
数据层(Model)
:去服务器请求数据,这里不只是云服务器的请求,数据库,文件,智能设备,任何数据源,只要是增删改查的,都属于数据层的工作。比如上述代码的网络库异步请求。
从分析来看,上述一个简单的需求实际上有三个层
的存在,而却全部写在View中,对于新手来说,这样类似的代码是再正常不过了。
一般来说,简单的需求,项目小,这样写也不会造成什么问题的,但是一旦项目越来越大,并且需求改动也越来越多的时候,就成了一种灾难了,比如无休止的复制和粘贴。
举个例子:
现在项目中加入了启动页,要求在启动页判断先前是否已有用户登录过,如果有,则取出账号密码进行登录,登录成功去到主界面,失败则去到登录页;
如果没有,直接跳转到登录页。
再用上面的写法,也就是加个启动页,然后复制登录的那段代码,再改改回调处理的,听起来好像没事,但不觉得重复了吗?
使用MVP模式重写
先看一张类图
你肯定会说:什么?一个简单的功能,居然需要这么多类文件,这不是更加烦琐,工作量更加大了吗?
别急,继续看。下面我们就按上面分析的来写。
LoginResponse.java :
JSON 解析需要的数据类
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.只有
登录
需要我们处理,其他的诸如输入账号密码,点击按钮这种操作�不需要我们做。至于账号密码的空判断,已经包含在登录
这个操作里了。 - 2.点击登录按钮后,需要显示
进度条
,登录成功
需要显示成功或直接去到主页面之类的,登录失败
需要隐藏进度条,�提示登录失败的原因之类的。
因此我们可以�把这些操作和显示都归类到一个地方,称为契约类(Contract)
。
LoginContract.java:
/**
* 登录契约类,声明了View和Presenter该有的操作,方便管理
*/
public interface LoginContract
{
// 定义界面中所有的 UI 状态
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 相关的具体类还没写。继续看。
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)
{
// 发起请求
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 类不负责逻辑的处理,只是负责增删改查
,以及必要的保存住自己的状态
,比如你这个 Model 表示一个智能开关设备,那么开关的状态你得保存起来,以便�状态改变的时候发出通知,以及外面的人来你这拿状态的时候,你得给人家正确的状态。
这里的 Model 表示登录,login 方法被调用后
,将登录结果通过 LoginCallback
回传给调用者就完成了职责。
再来看 View。
View:�LoginActivity.java:
public class LoginActivity
extends AppCompatActivity
implements LoginContract.View // 实现了�契约类中的接口
{
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);
// 创建Presenter,使View和Presenter,Presenter和Model关联起来,这一步暂且忽略也可以,等看到 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发起登录
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 中没有任何的逻辑处理和数据获取,能做的��只是跟界面相关的操作,向�外界发出请求,以及�暴露给外界操作界面的方法。注意:View 中不能有任何的业务逻辑处理,只能有和 View 相关的操作。
可以看到,Model 和 View 是完全分开的,没有任何的直接关联。下一步,我们需要通过 Presenter 将他们关联起来。
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("登录错误的提示信息");
}
}
});
}
}
可以看到,所有的业务逻辑都在 Presenter 里面了。
看看以上的代码是不是符合下面这张图:
Model 和 View 是完全分离的,以上通过小实例目的是为了让大家理解并用起来,更复杂彻底的 MVP ,可以查看 Google 的官方 Sample:
googlesamples/android-architecture
还有一个开源项目:
android10/Android-CleanArchitecture
MVP 的好处
-
Model 只有一个,View 只有一个,而 Presenter 可以有多个,但是一个 View 至少对应一个 Presenter,还是那句话,架构是很灵活的,你都把 Model 和 View 分开了,低耦合已经实现了,怎么关联他们,你看着办咯。
-
设计图出来了,接口还没好,你可以专注先写 View,完全不用管数据。设计图没好,接口好了,你可以先写 Model,测试接口是否正常,完全不用管 View 是如何设计的。等到都设计好了,�你再把 Model 和 View 关联起来专注写逻辑。是不是觉得无比的清爽?
-
非常适合于大型的项目,但是要避免过度设计和正确的抽象。
MVP 的坏处
- 类爆炸
结语:
本文的目的是让没有玩过 MVP 的设计快速入门的,�理解了以上内容,�进阶的内容可自己 Google,很多这方面的资料。