这段时间的感慨
其实在之前那一篇MVP模式中大概的对MVP模式做了一个阐述,但是实际运用中要考虑到蛮多的细节性问题 ,而且感觉这次MVP开发的十分顺畅,虽然用户端这边是我一个人开发,数了下大概100多个类,1w行代码的样子。怎么说呢,这次中软杯,由于我不是弄算法这块的,对于Android端我把我现在能做的最好水平做出来了。
虽然做到最后做的想吐了(一个人在各种model、view、presenter层进行切换接口来回调),但终于还是在昨晚12点撸完了第一期 ,其实身边一些新的技术新的事情一直都有在关注,比如最近google开发中大会上宣布kotlin成为官方的第一语言,所以也想在接下来抽出一段时间学习kotlin,可是最近确实忙的不成人形,虽然有时候听起来像借口,但是我可能目前是处于这种状态,就是做什么事情都喜欢一段一段时间的做,而且这一段时间会100%投入,比如学习,那么今天从起床就能学到睡觉,如果每天能够学习的时间不到两个小时,我可能就回去做别的事情了,其实这就是我的缺点吧,要是能够更加合理的规划时间就好了。最近又有个创业性的项目拉我进来负责Android用户端,因为投资人已经确认等到成品就可以投资了,可是当大牛找到我跟我阐述了项目大概要做的内容时,我的第一反应确实觉得如果接下来一段时间做这种类似类型的app不能很系统的学习而有点排斥,可是转念一想,这也算是个机会吧,人一辈子这么长时间,也不差这几个月,所以就答应大牛一起干了。
我觉得接下来的时间我应该要改变了,不管是对自己时间的分配还是别的什么,都要更加合理才行,不能因为自己觉得没时间就不学习了吧,因为只有做项目的时候为了有的东西焦头烂额的时候才知道对原理性的内容掌握不够,其实就算每天抽出半个小时看一些常用内容的源码,也是会有所收获的吧。好了,废话不多说,进入正题吧。
MVP项目框架搭建原则
- 对视图层、业务层和数据层进行解耦
- 从架构层避免内存泄漏、OOM等状况的发生
- 对子类层通用的细节抽象,非通用性细节先实现通过继承改写
- 对通用的组件进行引入,并保证其生命周期不会超过activity
MVP框架在项目中的应用
其实之前在我应用MVP的时候,我就一直苦苦的思考,什么样的写法才是正宗的mvp写法呢?思考很久也没有个答案,后来看到设计模式里的一段话,我想通了,其实包括对设计模式的应用,没有标准答案,书上罗列出的,只是一种简单的通用情况,实际的应用要根据具体情况来改变,如果严格遵循设计模式的原则,接口单一,可是实际情况下存在么,这是理想情况下最好的,可是实际下也要考虑开发情况开发成本等。包括这里的MVP,我觉得没有真正的答案,他可能罗列出一个通用解耦的情况,你可以对解耦进行进一步解耦,也可以修改,甚至有时候可以舍去某些层次,都是可以的。
mvp项目的时序图:
这里我以登录为例子,时序图如下:
这里可以很明显的看到方法在不同层次间的调用和回调,其实所有的网络业务,基本上就是类似这张图的层次,从view层发起操作,调用presenter层的中间者执行操作,在调用presenter层发起网络请求拿到结果数据(这里是服务器处理完成后的结果),根据数据结果调用监听回调view层操作来觉得view层该怎么做,全程都是一层一层解开耦合,各类分工合理各司其职,这就是mvp实际使用情况的一个时序图了。
MVP项目框架的搭建
这个项目框架其实就是最关键的,剩下的内容我会等比赛完毕上传到GitHub上,到时候会附上链接。首先我们看看MVP各层的接口是怎么写的
登录的View层接口
package com.we.piccategory.view;
import android.app.Dialog;
/** * Created with Android Studio * User: 潘浩 * School 南华大学 * Date: 2017/5/1 * Time: 0:39 * Description: */
public interface ILoginView {
void skip(Class clazz);
void showMsgDialog(String msg);
Dialog showLoading();
}
通用的presenter抽象类
package com.we.piccategory.mvp;
import com.we.piccategory.model.IModelImpl;
import java.lang.ref.WeakReference;
public abstract class BasePresenter<T> {
protected WeakReference<T> viewRef;
protected IModel model = new IModelImpl();
public void attachView(T view) {
viewRef = new WeakReference<T>(view);
}
public void detachView() {
if (viewRef != null) {
viewRef.clear();
viewRef = null;
}
}
protected T getView() {
return viewRef.get();
}
}
这里的泛型解释一下,传入的是View层的接口,比如登录操作传递的就是ILoginView。这边采用弱引用因为presenter层和view相互持有对方的引用,同时presenter层持有model层引用,我们的网络耗时操作实际上就是放在model中执行的,所以这里为了避免mvp层架构产生内存泄漏的问题从而采用了弱引用。
通用的Model层接口
package com.we.piccategory.mvp;
/** * Created by 86119 on 2017/3/21. */
public interface IModel {
/** * 获取数据 * * @param listener 监听回调 */
void load(OnCompletedListener listener);
interface OnCompletedListener<T> {
/** * 获取到数据并且数据要交互到界面成功时 * * @param data */
void onCompleted(T data);
/** * 仅仅提交请求不需要数据时成功 */
void onCompleted();
/** * 提交请求结果失败时 */
void onFail(String msg);
}
}
其实写到后来发现,这个不带参数的onSuccess方法没必要的赶脚,这里先写着就这样子都作为回调的接口方法。这里的泛型,是作为通用数据类型的一个回传,也就是如果有指定类型的数据要回传展示等,就能从OnCompletedListener接口中根据指定类型回传到界面层,从而保证数据在传递过程中的一致性。这么咋一看,好像还没关联的样子,别急,往下看
通用的Activity基类
package com.we.piccategory.ui.base;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import com.we.piccategory.builder.DialogBuilder;
import com.we.piccategory.mvp.BasePresenter;
import butterknife.ButterKnife;
public abstract class BaseActivity<V, T extends BasePresenter<V>> extends AppCompatActivity {
protected T myPresenter;
public DialogBuilder builder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//调用子类的方法获取persenter
myPresenter = createPresenter();
myPresenter.attachView((V) this);
setContentView(initRes());
ButterKnife.inject(this);
builder = new DialogBuilder();
builder.attachView(this);
initData();
}
protected abstract int initRes();
@Override
protected void onDestroy() {
super.onDestroy();
myPresenter.detachView();
builder.detachView();
}
/** * 创建presenter对象 * * @return 子类中的实例presenter对象 */
public abstract T createPresenter();
protected void initData() {
}
}
可以看到,BaseActivity是一个抽象的基类,把具体实现不同的任务交给子类实现,比如这里的initRes用来回传layout布局用来setContentView,这里的createPresenter用来创建指定子类的具体presenter类型。这里有两个泛型,其实很容易看出来,泛型V指的是实现View层接口的Activity,泛型T指的是抽象presenter的子类,其实这里我就采取了一个比较巧妙的办法,虽然按照原则是面向接口编程,但是presenter的父类是一个抽象类而不是一个接口,而且同一个presenter里的业务各不相同,参数各不相同,也没法定义通用的接口方法,那么就干脆不要接口就好了,因为在BaseActivity层表明需要的是presenter的子类,那么子类具体的方法我们这边是可以调用到的,那么也就没必要纠结接口不接口的问题了。这里面这onCreate中调用presenter的attachView方法利用弱引用获取到Activity的引用,同时在onDestroy中调用detachView将Activity的引用clear掉,从而避免了Activity内存泄漏的发生。包括Dialog,因为基本每个Activity都会使用到包括加载提示dialog等,所以这边也是以这种方式接入进来。那么我们再来看看具体的loginActivity怎么写,不过可能这里举的例子不是很好,因为刚好我这个Activity由于业务需要操作有些复杂,就是类似拉钩网那种,直接按钮切换布局而不是跳转Activity的,这边我们主要关注登录操作的流程
package com.we.piccategory.ui.activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.we.piccategory.MainActivity;
import com.we.piccategory.R;
import com.we.piccategory.builder.DialogBuilder;
import com.we.piccategory.presenter.LoginPresenter;
import com.we.piccategory.ui.base.BaseActivity;
import com.we.piccategory.util.CommonUtil;
import com.we.piccategory.util.LoginManger;
import com.we.piccategory.view.ILoginView;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/** * Created by 86119 on 2017/4/9. */
public class LoginActivity extends BaseActivity<ILoginView, LoginPresenter> implements View.OnClickListener, ILoginView {
//inject的是login界面的控件
@InjectView(R.id.user_name)
public EditText etUserName;//用户名
@InjectView(R.id.password)
public EditText etPassword;//密码
@InjectView(R.id.btn_login)
public Button loginBtn;//登录
@InjectView(R.id.btn_to_regist)
public Button toRegisterBtn;//跳转到注册的按钮
@InjectView(R.id.tv_forget)
public TextView forget;//忘记密码
//以下是register界面的控件
private EditText etPhone;
private EditText etCheck;
private Button register;
private Button toLoginBtn;
private TextView tvIdentify;
@Override
protected int initRes() {
return R.layout.activity_login;
}
@Override
protected void initData() {
boolean login_state = LoginManger.getLoginState();
if (login_state == true) {
skip(MainActivity.class);
}
}
@Override
public LoginPresenter createPresenter() {
return new LoginPresenter();
}
@OnClick({R.id.btn_login, R.id.btn_to_regist, R.id.tv_forget})
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
String userName = etUserName.getText().toString();
String password = etPassword.getText().toString();
myPresenter.login(userName, password);
break;
case R.id.btn_to_regist:
initToRegister();
break;
case R.id.tv_forget:
Intent intent = new Intent();
intent.setClass(LoginActivity.this, ForgetActivity.class);
startActivity(intent);
break;
case R.id.btn_to_login:
Log.i("ph", "aaa");
setContentView(R.layout.activity_login);
ButterKnife.inject(this);
break;
case R.id.btn_register:
String num = etPhone.getText().toString().trim();
String value = etCheck.getText().toString().trim();
myPresenter.checkValues(num, value);
// skip(RegisterInfoActivity.class);
break;
case R.id.tv_identify://验证码
String telNum = etPhone.getText().toString();
if (CommonUtil.checkNum(telNum, 11)) {
myPresenter.checkPhone(telNum);
}
break;
default:
break;
}
}
private void initToRegister() {
setContentView(R.layout.activity_register);
etPhone = (EditText) findViewById(R.id.phone_num);
etCheck = (EditText) findViewById(R.id.check_value);
register = (Button) findViewById(R.id.btn_register);
toLoginBtn = (Button) findViewById(R.id.btn_to_login);
tvIdentify = (TextView) findViewById(R.id.tv_identify);
register.setOnClickListener(this);
toLoginBtn.setOnClickListener(this);
tvIdentify.setOnClickListener(this);
}
@Override
public void skip(Class clazz) {
Intent intent = new Intent();
intent.setClass(LoginActivity.this, clazz);
if (etPhone != null) {
intent.putExtra("telNum", etPhone.getText().toString());
}
startActivity(intent);
this.finish();
}
@Override
public void showMsgDialog(final String msg) {
AlertDialog dialog = builder.createLoadingDialog(msg, DialogBuilder.COMMON_MODEL);
dialog.show();
}
@Override
public Dialog showLoading() {
return builder.createLoadingDialog("", DialogBuilder.LOADING_MODEL);
}
}
然后是具体对应的LoginPresenter
package com.we.piccategory.presenter;
import android.app.Dialog;
import android.util.Log;
import com.we.piccategory.MainActivity;
import com.we.piccategory.bean.Token;
import com.we.piccategory.decorator.CheckModelDecorator;
import com.we.piccategory.decorator.CheckValuesDecorator;
import com.we.piccategory.decorator.LoginModelDecorator;
import com.we.piccategory.decorator.TelChangeDecorator;
import com.we.piccategory.mvp.BasePresenter;
import com.we.piccategory.mvp.IModel;
import com.we.piccategory.ui.activity.RegisterInfoActivity;
import com.we.piccategory.util.LoginManger;
import com.we.piccategory.view.ILoginView;
public class LoginPresenter extends BasePresenter<ILoginView> {
/** * 登录操作 * * @param userName 用户名 * @param password 密码 */
public void login(final String userName, String password) {
//获取装饰类
model = new LoginModelDecorator(model, userName, password);
final ILoginView iLoginView = viewRef.get();
final Dialog dialog = iLoginView.showLoading();
dialog.show();
model.load(new IModel.OnCompletedListener<Token>() {
@Override
public void onCompleted(Token token) {
dialog.dismiss();
//登录成功以后将token值写入本地
String tokenValue = token.getToken();
Integer userId = token.getUserId();
LoginManger.setLoginState(tokenValue, userId, true);
//跳转界面
iLoginView.skip(MainActivity.class);
}
@Override
public void onCompleted() {
}
@Override
public void onFail(String msg) {
dialog.dismiss();
iLoginView.showMsgDialog(msg);
}
});
}
/** * 校验手机号是否正确 * 请求 * * @param telNum 手机号 */
public void checkPhone(String telNum) {
model = new CheckModelDecorator(model, telNum);
final ILoginView iLoginView = viewRef.get();
model.load(new IModel.OnCompletedListener() {
@Override
public void onCompleted(Object data) {
}
@Override
public void onCompleted() {
Log.i("ph", "onCompleted: 获取验证码成功");
}
@Override
public void onFail(String msg) {
iLoginView.showMsgDialog(msg);
}
});
}
/** * 校验手机号和验证码是否正确 * 这里的请求是发送给mob第三方 * * @param telNum * @param identifyNum */
public void checkValues(String telNum, String identifyNum) {
//发送获取验证码的指令
model = new CheckValuesDecorator(model, telNum, identifyNum);
final ILoginView iLoginView = viewRef.get();
model.load(new IModel.OnCompletedListener<String>() {
@Override
public void onCompleted(String data) {
iLoginView.skip(RegisterInfoActivity.class);
}
@Override
public void onCompleted() {
}
@Override
public void onFail(String msg) {
iLoginView.showMsgDialog(msg);
}
});
}
public void changeNum(final String telNum, String identifyNum) {
//发送获取验证码的指令
model = new CheckValuesDecorator(model, telNum, identifyNum);
final ILoginView iLoginView = viewRef.get();
model.load(new IModel.OnCompletedListener<String>() {
@Override
public void onCompleted(String data) {
//当第三方校验通过时
changeTel(telNum, iLoginView);
}
@Override
public void onCompleted() {
}
@Override
public void onFail(String msg) {
iLoginView.showMsgDialog(msg);
}
});
}
public void changeTel(final String telNum, final ILoginView iLoginView) {
model = new TelChangeDecorator(model, telNum);
model.load(new IModel.OnCompletedListener<String>() {
@Override
public void onCompleted(String text) {
LoginManger.setTelNum(telNum);
if (iLoginView != null) {
iLoginView.showMsgDialog(text);
}
}
@Override
public void onCompleted() {
}
@Override
public void onFail(String msg) {
if (iLoginView != null) {
iLoginView.showMsgDialog(msg);
}
}
});
}
}
同样,因为界面需要一个presenter的不可能只做一件单一的事情,所以包含的别的业务操作,但是你可以看到每个方法除了参数基本的操作类似,也就是拿到结果回传或者做数据的回显。这里也是主要关注login这个方法。再看model层是如何表现的
package com.we.piccategory.decorator;
import com.loopj.android.http.RequestParams;
import com.we.piccategory.bean.RgbResult;
import com.we.piccategory.bean.Token;
import com.we.piccategory.mvp.IModel;
import com.we.piccategory.net.HttpUtil;
import com.we.piccategory.net.SuccessRespHandler;
import com.we.piccategory.ui.base.BaseApp;
import com.we.piccategory.util.Constant;
import com.we.piccategory.util.MD5Util;
import com.we.piccategory.util.RSAUtils;
import java.io.InputStream;
public class LoginModelDecorator extends ModelDecorator {
private String userName;
private String password;
public LoginModelDecorator(IModel model, String userName, String password) {
super(model);
this.userName = userName;
this.password = password;
}
@Override
public void load(OnCompletedListener listener) {
this.login(listener);
}
private void login(final OnCompletedListener listener) {
//校验数据
try {
String dest = "/user/login";
RequestParams params = new RequestParams();
params.put("userName", userName);
...//加密过程这里注释掉
params.put("password", encrypt);
params.put("md5", md5);
HttpUtil.doPost(dest, params, new SuccessRespHandler(listener) {
@Override
protected RgbResult getResult(String resp) {
return RgbResult.formatToPojo(resp, Token.class);
}
@Override
protected void onStatusFail(RgbResult rgbResult) {
mListener.onFail("用户名或密码不正确");
}
@Override
protected void onStatusOk(RgbResult rgbResult) {
Token token = (Token) rgbResult.getData();
mListener.onCompleted(token);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里用了个装饰者模式,其实吧,是脑子抽抽了,本来想的是presenter层没接口就用装饰者增加presenter的功能,可是最后想明白这边反而脑子抽抽用了装饰者,其实在这边作用并不大,好吧,这不是重点,看这个类实际的内容,就是访问网络请求结果,成功了会返回一个Token类对象,我这边拿到Token对象所以Presenter的匿名内部OnCompleted类传的也是Token,这边就刚好做一个数据的回传,如果view层需要,接口就可以给定数据类型来拿到数据展示或其它功能等。
结语 好啦,以上就是MVP模式可以在实际项目中应用的情况,这里以一个简单的mvp模式进行登录给大家讲解mvp的具体使用情况,当然,这只是我在我的项目中的使用情况,具体可以结合RxJava等一起使用,也可根据自己的实际项目情况灵活的加以运用,总之呢,知识是死的,人是活的,要学会活学活用,才能够让自己适应各种情况,挺晚的,该睡了,晚安了各位。