其实MVP已经不算新东西了,写这篇文章的目的主要是为了把之前在项目重构中应用的MVP模式做一个整理、总结和记录,把实践代码做成一个可描述可理解的过程。
一篇可能写不完,总共打算分几篇来记录,这将包括且不限于以下内容:
说起MVP就不得不提起MVC, 因为MVP的是在MVC的基础上优化而来的:
MVC角色说明:
角色 | 职责 |
---|---|
View | 视图界面层,与用户发生交互,接收用户输入的请求转发给Controller处理 |
Controller | 接收View的请求, 从视图层获取数据,执行业务逻辑,并调用Model层进行数据存取 |
Model | 执行数据存取的业务逻辑,并根据业务模型通知视图层更新(一般是通过观察者模式) |
MVP角色说明:
角色 | 职责 |
---|---|
View | 视图界面层,与用户发生交互,接收用户输入的请求转发给Presenter处理 |
Presenter | 接收View的请求, 从视图层获取数据,执行业务逻辑,并调用Model层进行数据存取,同时会调用View层的接口将数据更新到视图 |
Model | 执行数据存取的业务逻辑,并将数据返回给Presenter层 |
可以看到MVP与MVC最大的区别就是MVP解耦了View层和Model层,两者不直接发生交互而是通过P层,而在MVC中Model还是会跟View层发生交互的。
传统的MVC模式更加适合于大型项目的开发,对于小型项目应用MVC反而显得臃肿繁琐,就像我们以前经常在Activity中最喜欢写的代码一样:UI处理、网络请求、数据存取等全部业务都放在Activity中来完成,这就像一个大杂烩,此时的Activity几乎兼顾了MVC中的所有角色,实际上在我看来它此时根本就不具备任何模式可言。而MVP模式的出现将臃肿的部分分解开来,恰好适合于小型的应用程序,比如移动端的应用,并且对单元测试也比较友好,所以大家都在提倡用这个模式进行开发。
按照惯例,还是从一个简单的登录页面来演示MVP的基本使用(几乎我看过的所有关于MVP的文章貌似都是拿登录页面来入门的。。)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<EditText
android:id="@+id/edit_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:hint="请输入用户名"
android:textSize="16sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edit_user_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1"
android:hint="请输入密码"
android:textSize="16sp"
android:textColor="@color/black" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="登录"
android:textSize="16sp"
android:textColor="@color/black"
/>
LinearLayout>
LoginActivity代码:
public class LoginActivity extends Activity implements View.OnClickListener {
private EditText mUserNameEdit;
private EditText mUserPasswordEdit;
private Button mLoginBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView();
}
private void initView() {
mUserNameEdit = (EditText) findViewById(R.id.edit_user_name);
mUserPasswordEdit = (EditText) findViewById(R.id.edit_user_password);
mLoginBtn = (Button) findViewById(R.id.btn_login);
mLoginBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
//TODO
break;
default:
break;
}
}
}
如果是传统的MVC的话,到这一步基本就结束了,接下来就开始在LoginActivity中搞事情作死了。。
如果使用MVP的话,接下来我们需要建立几个package来存放对应的角色:
其中view包下存放activity、fragment等UI控件,iview包下存放presenter与view进行交互的接口类,我们把LoginActivity放入view/activity包中:
其他包下面的情况:
在MVP中为了解耦Presenter跟View和Model的交互都通过接口进行,所以新建一个跟LoginActivity
交互的接口类ILoginView
:
public interface ILoginView {
/** 获取输入框的登录用户名 */
String getUserName();
/** 获取输入框的用户密码 */
String getUserPassword();
/** 显示Toast提醒 */
void showToast(String msg);
/** 登录成功的UI处理 */
void onLoginSuccess();
/** 登录失败的UI处理 */
void onLoginFail();
/** 显示加载中弹窗 */
void showProgressDialog();
/** 隐藏加载中弹窗 */
void hideProgressDialog();
/** 获取当前UI页面的上下文 */
Context getContext();
}
view
接口类中定义的基本都是一些UI
数据或者显示UI
控件的方法,然让LoginActivity
实现这个接口:
public class LoginActivity extends Activity implements ILoginView, View.OnClickListener {
private EditText mUserNameEdit;
private EditText mUserPasswordEdit;
private Button mLoginBtn;
public ProgressDialog mProgressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView();
}
private void initView() {
mUserNameEdit = (EditText) findViewById(R.id.edit_user_name);
mUserPasswordEdit = (EditText) findViewById(R.id.edit_user_password);
mLoginBtn = (Button) findViewById(R.id.btn_login);
mLoginBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
//TODO
break;
default:
break;
}
}
@Override
public String getUserName() {
return mUserNameEdit.getText().toString();
}
@Override
public String getUserPassword() {
return mUserPasswordEdit.getText().toString();
}
@Override
public void showToast(String msg) {
ToastUtils.showToast(this, msg);
}
@Override
public void onLoginSuccess() {
ToastUtils.showToast(this, "登录成功");
//跳转首页
gotoHomeActivity();
finish();
}
@Override
public void onLoginFail() {
ToastUtils.showToast(this, "登录失败");
}
@Override
public void showProgressDialog() {
if (mProgressDialog == null) {
mProgressDialog = DialogUtils.showSpinningProgressDialog(this, "正在登录中...", false);
} else {
if (!mProgressDialog.isShowing()) {
mProgressDialog.show();
}
}
}
@Override
public void hideProgressDialog() {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
@Override
public Context getContext() {
return this;
}
/** 跳转首页 */
private void gotoHomeActivity() {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
}
View
层到此就完事了,接下来实现Presenter
和Model
角色的接口和实现类
Presenter
接口和实现类:
public interface ILoginPresenter {
/** 登录操作逻辑处理 */
void login();
/** 登录成功逻辑处理 */
void onLoginSuccess();
/** 登录失败逻辑处理 */
void onLoginFail(String errMsg);
}
public class LoginPresenter implements ILoginPresenter {
private ILoginView mLoginView;
private ILoginModel mLoginModel;
public LoginPresenter(ILoginView loginView) {
mLoginView = loginView;
mLoginModel = new LoginModelImpl(this);
}
@Override
public void login() {
// 对用户名和密码的校验逻辑,这里只简单判空,实际可以加更多校验
if (TextUtils.isEmpty(mLoginView.getUserName())) {
mLoginView.showToast("请输入用户名");
return;
}
if (TextUtils.isEmpty(mLoginView.getPassword())) {
mLoginView.showToast("请输入密码");
return;
}
//判断网络是否可用
if (!NetUtils.checkNetState(mLoginView.getContext())) {
mLoginView.showToast("当前无网络连接,请检查网络");
return;
}
//显示登录进度弹窗
mLoginView.showProgressDialog();
//调用model层发起登录请求
mLoginModel.sendLoginRequest(mLoginView.getUserName(), mLoginView.getPassword());
}
/** 登陆成功 */
@Override
public void onLoginSuccess() {
//隐藏登录进度弹窗
mLoginView.hideProgressDialog();
//回调View层接口
mLoginView.onLoginSuccess();
}
/** 登陆失败 */
@Override
public void onLoginFail(String errMsg) {
//隐藏登录进度弹窗
mLoginView.hideProgressDialog();
//回调View层接口
mLoginView.onLoginFail(errMsg);
}
}
可以看到在LoginPresenter
的实现类当中分别持有了ILoginView
和ILoginModel
两个接口,LoginPresenter
通过这两个接口分别与LoginActivity和LoginModel进行交互。其中ILoginView
变量是通过构造函数传递进来的,而ILoginModel
则是在构造函数内部创建的。
Model
接口和实现类:
public interface ILoginModel {
/** 发起登录请求 */
void sendLoginRequest(String userName, String password);
}
public class LoginModelImpl implements ILoginModel {
private static final String API_LOGIN = "/mobile/login";
private ILoginPresenter mPresenter;
public LoginModelImpl(ILoginPresenter mPresenter) {
this.mPresenter = mPresenter;
}
@Override
public void sendLoginRequest(String userName, String password) {
String url = BuildConfig.BASE_IP + API_LOGIN;
StringHashMap requestParams = new StringHashMap();
requestParams.put("userName", userName);
requestParams.put("password", password);
HttpDataManager.post(url, requestParams, new HttpCallback() {
@Override
public void onSuccess(String result, Object tag) {
LoginResultBean loginResult = JsonUtils.jsonToObject(result, LoginResultBean.class);
if (loginResult != null) {
if (loginResult.getErrCode() == 200) {
mPresenter.onLoginSuccess();
} else {
mPresenter.onLoginFail(loginResult.getErrMsg());
}
} else {
mPresenter.onLoginFail("登录失败,服务不可用");
}
}
@Override
public void onError(String result, Object tag) {
mPresenter.onLoginFail("登录失败:"+result);
}
}, 100);
}
}
在LoginModelImpl
实现类中持有了ILoginPresenter
接口,通过它与LoginPresenter
实现类进行交户。在LoginModelImpl
实现类中发起请求调用的是HttpDataManager
类的post方法,这个类实际上就是你的网络请求框架组件,这里用的是基于开源库的一个简单封装(内部其实还是用的OkHttpClient)。
HttpDataManager类的代码:
public class HttpDataManager {
/** 发起get请求 */
public static void get(String url, StringHashMap params, HttpCallback callback, int tag) {
RequestParams requestParams = new RequestParams();
requestParams.setAllStrParams(params);
BaseHttpRequest request = new BaseHttpRequest(url, requestParams, callback, tag);
HttpClient.getInstance().get(request);
}
/** 发起post请求 */
public static void post(String url, StringHashMap params, HttpCallback callback, int tag) {
RequestParams requestParams = new RequestParams();
requestParams.setAllStrParams(params);
BaseHttpRequest request = new BaseHttpRequest(url, requestParams, callback, tag);
HttpClient.getInstance().post(request);
}
}
这里建了两个类HttpDataManager
和LocalDataManager
分别用来管理网络请求和本地数据存取(File、SharePrefer、DataBase),这里你可以使用流行的开源网络请求库或者其他数据管理库(如Retrofit、GreenDao等)。当然对于一个好的架构模式不应该受限于这些库,你可以随意选取。
最后就是在LoginActivity当中调用LoginPresenter的方法进行登录操作了:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mLoginPresenter = new LoginPresenter(this);
initView();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
//开始登录
mLoginPresenter.login();
break;
default:
break;
}
}
到此,一个简单登录页面的MVP模式的简单实现就完成了。
如果是第一次接触MVP,你会明显的发现类文件比传统的写法陡增啊!是的,没错,这也是MVP的缺点之一,一个页面要想用MVP模式做的比较完美至少要建立五、六个Java类,因为Model层和Presenter层要交互,Presenter层和View层要交互,每个交互都需要接口来进行,即便你使用一些依赖注入框架,可能减少new对象的代码,但是Java类文件数量依然是少不了的,这是由MVP的角色和角色职责功能决定的。
所以你可能感觉原来在一个Activity当中就能完成的工作,现在要分这么多文件,但是这样做的代价就是换来了业务逻辑的解耦和职责的划分清晰,别忘了我们当初是为了什么目的来用这个模式的!任何一个模式都会有利有弊,有奉献就会有牺牲,你不可能做到只利用它的优点而不接受它的缺点影响。
这也是为什么前面提到MVP模式不适合大型项目的开发,因为大型项目本来类文件就多,你再用MVP类文件要翻好几倍,顿时就炸了,所以说对移动端的应用比较适合,因为移动端的app程序一般都比较小。
MVP的优点和缺点:
优点:
缺点:
当然上面的例子是比较理想的状态,实际项目中可能不一定按照完美的实现过程来做,例如,假如一个页面更新/获取view的地方掺杂了业务逻辑特别的多超过了50个,那么你要对应的去写一套接口的话估计要疯掉,这就是理想与现实的一个的差距,后面的文章中单独来分析如何在理想与现实之间做一个妥协。
在实际项目中MVP的每一层其实可以做纵向的复用代码提取,在每一层中也可以再进行分类,比如根据业务模型再去划分等等,具体后面文章再说吧。
下一篇将记录总结:MVP模式中如何分类,哪些属于View层,哪些该放在Presenter层,哪些该放在Modle层