MVP之高级MVP架构封装
No MVP:
我们一般会这样写:
public class MainActivity extends AppCompatActivity {
private EditText etAccount,etPassWord;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getSharedPreferences("account_sp",MODE_PRIVATE).getBoolean("hadLogin",false)){
Toast.makeText(this,"已经登录了就不用再登录啦",Toast.LENGTH_SHORT).show();
startActivity(new Intent(MainActivity.this,SQLiteActivity.class));
}
etAccount = findViewById(R.id.et_account);
etPassWord = findViewById(R.id.et_pw);
findViewById(R.id.btn_login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String strAccount = etAccount.getText().toString();
String strPW = etPassWord.getText().toString();
login(strAccount,strPW);
}
});
}
private void login(String strAccount,String strPW){
//模拟执行网络访问,发起登录请求
//模拟网络请求数据的延迟,让当前线程(UI线程)暂停4秒
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SharedPreferences sp1 = getSharedPreferences("account_sp",MODE_PRIVATE);
if (strAccount.equals(sp1.getString("account","0"))
&&strPW.equals(sp1.getString("password","0"))){
//如果登录成功
SharedPreferences.Editor editor = sp1.edit();
editor.putBoolean("hadLogin",true);
editor.commit();
Toast.makeText(this,"登录成功",Toast.LENGTH_SHORT).show();
startActivity(new Intent(MainActivity.this,SQLiteActivity.class));
finish();
}else {
//如果登录失败
Toast.makeText(this,"帐号或密码错误",Toast.LENGTH_SHORT).show();
}
}
}
上面的代码是最原始的写法,我们将所有的工作都全部写在了工作Activity中,对于小型项目来说(就比如这个登录),这样写其实更简单直接,但是当项目的业务需求扩展到一定程度的时候,如果继续使用这种写法,缺点如下:
- 1.代码逻辑复杂且代码量大(一套完整的登录注册修改密码模块可能代码量就会有数百行,试想一下一个类中堆积了几百行绕来绕去的代码后期维护会是什么心情)
- 2.Activity我们称之为界面,它的工作是显示数据,更新界面,响应用户操作事件,可是在这里,我们还让它承担了网络数据获取,数据解析,业务逻辑处理等工作,这大大的超出了它的本职范围。
MVC:
提到MVP就不得不提到MVC,关于MVC架构,可以看下面这张图
Model View Controller,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见下图当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。
对照上面的登录demo,activity_main.xml里面的xml文件就对应于MVC的view层,里面都是一些view的布局代码,而各种java bean类就对应于model层(假设我们将account和password抽出组合成一个类user,user就是model层),而controller层,就是MainActivity了。
这样解释好像是把一个项目严格的按照MVC三层架构给分开了,但是,结果真的是这样吗?
对button的点击事件是Controller,将account和password抽离组成的user是Model,View呢?V层的工作真的配得上与MC层齐名吗?
而这样写的问题就在于xml作为view层,控制能力实在太弱了,假设在注册成功之前我想弹出一个popupwindow提示用户阅读勾选使用协议,如果不勾选就不给注册,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。
MVC还有一个重要的缺陷,从图中可以看出view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。由于没有相关“大型”项目经验,就不展开赘述。
总而言之最直观的一点,好像我们是给项目分了MVC三层,但实际上呢?只有MC两层,甚至说,只有C(Activity)一层。
普通MVP:
所以基于MVC架构的优化MVP出现了:对于Android来说,MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。
从图中可以看出,MVP解决了V和M层存在耦合的问题。虽然V层和M层解耦了,但是V层和P层不是耦合在一起了吗?其实不是的,对于V层和P层的通信,我们是通过接口实现的,具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的Presenter中通过接口调用方法,换言之,V和P层的关系是可控的。不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。
- 1.首先我们先定义一个接口,用来规定针对这个界面逻辑View需要作出的动作的接口。
- 2.让Activity实现这个接口中的方法,也就是V层。
- 3.创建一个类,用来封装之前的网络请求过程,也就是M层
- 4.再创建一个类,用来处理M层和V层之间的通信,也就是P层
现在,我们就来实现这么一个架构:
View层接口:
首先我们先定义一个接口,用来规定针对这个界面逻辑View需要作出的动作的接口。
public interface LoginView {
//请求登录的时候展示加载(比如说展示progressBar)
void Logining();
//请求登录成功,一般后台会返回一个User信息的json数据
void LoginSuccess(User user);
//请求登录失败,(比如弹出一个错误提示框)
void LoginFailure(User user);
}
如何去设计View层接口,根据实际需求,根据View层对应的显示逻辑是什么去设计,比如说,一个注册功能(包含有图形验证码的验证),我需要响应:
- 验证码展示
- 验证码验证成功后可以注册
- 验证码验证失败弹出提示
- 注册成功
- 注册失败
- 其他操作……
那么我们就需要至少5个方法一一对应View层的显示逻辑。
View层:
View层实现对应接口的方法即可,在里面处理各种View的逻辑。
值得注意的地方在:
- 1.Activity需要实现v层接口
- 2.在实现类view中创建persenter
public class MVPLoginActivity extends AppCompatActivity implements LoginView{
private LoginPresenter loginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
//初始化presenter
loginPresenter = new LoginPresenter(this);
//调用presenter管理的登录方法
loginPresenter.clickToLogin(new User(etAccount.getText().toString(),etPassWord.getText().toString()));
}
@Override
public void Logining() {
//展示正在登录;
}
@Override
public void LoginSuccess(User user) {
//登录成功后展示成功的信息
}
@Override
public void LoginFailure(User user) {
//失败后展示失败的信息
}
}
Model层:
创建一个类,用来封装之前的网络请求过程,也就是M层,M层真正访问数据的地方,比如说在这里完成向后台获取数据,解析数据,然后可以返回给Presenter,交由Presenter中转View显示数据。
public class LoginModel {
public User Login(LOGIN_ARGS){
if (LOGIN_SUCCESS){
//模拟后台返回成功的数据
return SUCCESS_INFO;
}else if(LOGIN_FAILED){
//模拟后台返回失败的数据
return FAILED_INFO;
}
}
}
Presenter层:
再创建一个类,用来处理M层和V层之间的通信即Presenter层,调用Model层给的数据相关接口实现登录或者数据获取等操作,桥接对应的View层逻辑。
1.Persenter需要持有v层引用和m层引用
public class LoginPresenter {
private LoginView loginView;
private LoginModel loginModel;
public LoginPresenter(LoginView loginView){
this.loginView = loginView;
loginModel = new LoginModel();
}
public void clickToLogin(User user){
//提示正在登录
loginView.Logining();
//通过Model层登录
User user1 = loginModel.Login(user.getAccount(), user.getPassword());
//登录成功
{loginView.LoginSuccess(user1);}
//登录失败
{loginView.LoginFailure(user1);}
}
}
普通MVP的小结:
在使用MVP架构之后,我们的Activity不在是啥都做了,里面的逻辑很清晰,每一种操作都对应到了封装的方法可是这样写会内存泄露,例如在网络请求登录数据的过程中Activity就关闭了,Presenter对象还持有了V层的引用,也就是MVPLoginActivity,就会导致内存泄露。因为这里的Demo很简单,无法体现这种影响。
更形象的描述是:当Presenter对象持有一个或多个大型Activity的引用,如果该对象(P)不能被系统回收,那么当这些Activity不再使用时,这个Activity也不会被系统回收,这样一来便出现了内存泄漏的情况。在应用中内出现一次两次的内存泄漏或许不会出现什么影响,但是在应用长时间使用以后,若是这些占据大量内存的Activity无法被GC回收的话,最终会导致OOM的出现,就会直接Crush应用。
解决了内存泄露的MVP:
实现的思路就是,我们将Presenter的生命周期和View层的生命周期绑定在一起,给Presenter定义两个方法,一个绑定View层,一个解绑View层,在需要的时候进行绑定,不需要的时候进行解绑就可以了。
之前是在Presenter的构造方法中传递View层,那么现在不需要在构造函数中传递V层了,直接在需要创建Presenter的地方使用绑定的方法即可,在Activity的onDestroy方法中进行解绑定。
修改后的Presenter层:
//绑定
public void attachView(LoginView loginView){
this.loginView = loginView;
}
//解绑定
public void detachView(){
loginView = null;
}
public void interruptRequest(){
//中断model的网络请求
loginModel.interruptRequest();
}
我们给Presenter层额外设置了三个方法,分别是绑定和解绑定,以及在某些情况下我们需要中断网络请求操作的方法,在该方法中中断Model层的网络访问。来实现三层同步。
修改后的View层:
public class MVPLoginActivity extends AppCompatActivity implements LoginView{
@Override
protected void onCreate(Bundle savedInstanceState) {
//loginPresenter = new LoginPresenter(this);
loginPresenter = new LoginPresenter();
loginPresenter.attachView(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
loginPresenter.detachView();
loginPresenter.interruptRequest();
}
}
## 继续优化后的MVP:
在上面一步中,通过给Presenter层设置绑定和解绑View层的对应方法,解决了内存泄露的问题,但是这样并不够,一个APP中肯定不可能只有一个简单的登录模块,基于MVP使得我们每个功能模块都对应着一个V层和P层,那这样的话每个Presenter中都要定义绑定和解绑的方法,而Activity中对应的也要调用这绑定和解绑的两个方法,在这里导致了另一个问题:代码冗余。
要解决这个问题我们可以抽取出一个基类的Presenter和一个基类的Activity来做这个事情,让子类不用在写这些重复的代码。
那么随之而来的就是另一个问题:既然是基类,肯定不止有一个子类来继承它,子类当中定义的View接口和需要创建的Presenter都各不相同,我们无法将基类写死,解决的办法就是使用泛型。
基类View层接口:
创建一个基类接口BaseView,这个View可以什么都不做只是用来约束类型的
public interface BaseView {
}
基类Presenter:
创建一个基类的BasePresenter,在类上规定View的泛型,然后定义绑定和解绑的抽象方法,让子类去实现,对外在提供一个获取View的方法,
让子类直接通过方法来获取View
public abstract class BasePresenter<V extends BaseView> {
private V mMvpView;
public void attachView(V view){
this.mMvpView = view;
}
public void detachView(){
mMvpView = null;
}
public V getmMvpView() {
return mMvpView;
}
}
基类View层:
创建一个抽象基类的BaseActivity,声明一个创建Presenter的抽象方法,因为要帮子类去绑定和解绑那么就需要拿到子类的Presenter才行,但是又不能随便一个类都能绑定的,因为只有基类的Presenter中才定义了绑定和解绑的方法,所以同样的在类上可以声明泛型在,方法上使用泛型来达到目的。
public abstract class BaseActivity<V extends BaseView,P extends BasePresenter<V>>
extends AppCompatActivity implements BaseView{
private P presenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (presenter==null){
presenter = createPresenter();
presenter.attachView((V) this);
}else {
presenter.attachView((V) this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (presenter!=null){
presenter.detachView();
}
}
public abstract P createPresenter();
public P getPresenter() {
return presenter;
}
}
最后三层各自继承各自的基类即可
总结
从MVC到最简单的MVP架构,我们解决了MVC的数据层和视图层耦合的问题;随之而来的是内存泄露的问题,通过设置对应的绑定解绑方法来解决这个问题;之后又是代码冗余的问题,于是利用Java的多态性,我们将重复性工作交由基类去完成,子类继承基类重写对应方法即可。而实际上
我们只需要修改上面Presenter中的构造代码,不需要在构造中传递V层了,然后再写一个绑定和解绑的方法,最后修改Activity创建Presenter时进行绑定,在onDestroy中进行解绑。