2016-05-12 15:19:45 aishang5wpj 阅读数 1433

前言

前些天,在公司实习的测试妹纸回学校答辩了,要我给她做个毕业设计,说要求不高,看看界面就行。

七搞八搞给做了个,但是项目一开始并不是用mvp写的,因为那时感觉对mvp的理解还不是很深刻。前两天有空,就把它改成了mvp风格的样子,所以也就有了这篇博客。

网上也有很多文章跟demo,初看时始终不得其解,后来在将那个项目改成mvp的过程中才开始有了拨云见月的感觉,这篇博客也将尽可能用通俗易懂的语言来帮助更多像我一样的初学者更好的理解什么是Mvp。

纸上得来终觉浅,绝知此事要躬行。建议大家还是先自己动手,写一个登录注册的demo,然后把它改成mvp的风格,在改的过程中,你也就能慢慢理解里面的思想了。

M

Model,即数据对象跟业务逻辑。在学校学Java EE的时候,项目里总是有很多的DTO、DAO对象,如果把我们项目中的XXBean看做DTO的话,那么这里的Model就可以看成是Java EE里的DAO了。

假设这里有一个UserBean的话,那么与之对应我们就应该对应一个UserModel,而设计模式讲究:

依赖接口编程,而不是依赖细节编程

所以我们一般先定义一个IUserModel接口,然后UserModel来实现它。

IUserMode.java

package com.baobeikeji.shower.bean.imodel;

import com.baobeikeji.shower.bean.UserBean;

/**
 * Created by wpj on 16/5/10上午10:26.
 * 处理数据、业务逻辑
 */
public interface IUserMode {

    UserBean loadUser(String id);

    UserBean loadUser(String id, String name, String password, String schoolName, String question, String answer);
}

UserModel.java

package com.baobeikeji.shower.bean.model;

import android.text.TextUtils;

import com.baobeikeji.shower.bean.UserBean;
import com.baobeikeji.shower.cache.CacheManager;
import com.baobeikeji.shower.bean.imodel.IUserMode;
import com.baobeikeji.shower.bean.imodel.ISchoolMode;
import com.baobeikeji.shower.bean.imodel.IValidateMode;
import com.google.gson.Gson;

/**
 * Created by wpj on 16/5/10上午10:34.
 */
public class UserModel implements IUserMode {
    private Gson mGson;
    private ISchoolMode mSchoolMode;
    private IValidateMode mValidateMode;

    public UserModel() {
        mGson = new Gson();
        mSchoolMode = new ScoolMode();
        mValidateMode = new ValidateMode();
    }

    @Override
    public UserBean loadUser(String id) {
        UserBean userBean = new UserBean(id);
        //CacheManager是自己定义的一个缓存工具类
        String result = CacheManager.getManager().onReadFromCache(userBean);
        if (TextUtils.isEmpty(result)) {
            return null;
        }
        return mGson.fromJson(result, UserBean.class);
    }

    @Override
    public UserBean loadUser(String id, String name, String password, String schoolName, String question, String
            answer) {

        //可以完成 校验参数合法性,设置默认值等操作
        UserBean userBean = new UserBean();
        userBean.id = id;
        userBean.name = name;
        userBean.password = password;
        userBean.scool = mSchoolMode.loadSchool(schoolName);
        userBean.addValidateBean(mValidateMode.loadValidateBean(question, answer));
        return userBean;
    }
}

IUserModel封装一些对UserBean各种增删改查的操作,而一开始里面有哪些方法我们是不知道的,所以后面根据实际需要在里面添加。

V

View,即展示给用户的界面。在以往mvc风格开发的app中,view就是我们的activity,然后在大家日常的开发中,习以为常的把各种网络请求、数据处理等操作都放在activity、fragment中,使其动辄成百上千行,这使我们后面的维护任务相当艰巨。

设计模式讲究单一职责原则,既然activity作为view,那就只应该处理好自己的本分职责,而不应该既负责显示UI又要负责处理数据。

在Mvp中,数据处理部分交由Model来实现,而View只负责UI显示。那Model处理数据的时候需要显示UI怎么办呢?

假设有一个LoginActivity,与之对应我们应当新建一个ILoginView接口,并在其中声明各种与UI交互的方法,比如显隐dialog、get/set文本框等等。

ILoginView.java

package com.baobeikeji.shower.login;

import com.baobeikeji.shower.bean.UserBean;

/**
 * Created by wpj on 16/5/10 上午 10:26.
 * <p>
 */
public interface ILoginView {

    String getUsername();

    String getPassword();

    void loginSuccessed(UserBean userBean);

    void showToast(String msg);
}

那谁来实现这些方法呢?那就是我们的LoginActivity。

LoginActivity.java

package com.baobeikeji.shower.login;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;

import com.baobeikeji.shower.MainActivity;
import com.baobeikeji.shower.R;
import com.baobeikeji.shower.app.BaseActivity;
import com.baobeikeji.shower.bean.UserBean;
import com.baobeikeji.shower.login.forgetpassword.ForgetPasswordActivityMvp;
import com.baobeikeji.shower.login.register.RegisterActivityMvp;

public class LoginActivityMvp extends BaseActivity implements ILoginView {

    private EditText mUsernameEt, mPasswordEt;
    private LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
    }

    @Override
    public void onInitViews() {
        mUsernameEt = (EditText) findViewById(R.id.login_username_et);
        mPasswordEt = (EditText) findViewById(R.id.login_password_et);
    }

    @Override
    public void onInitListeners() {

        findViewById(R.id.login_regist_tv).setOnClickListener(this);
        findViewById(R.id.login_forget_password_tv).setOnClickListener(this);
        findViewById(R.id.login_submit_btn).setOnClickListener(this);
    }

    @Override
    public void onInitData() {

        mLoginPresenter = new LoginPresenter(this);
    }

    @Override
    public void onClick(View view) {

        Intent intent;
        switch (view.getId()) {
            case R.id.login_regist_tv:

                intent = new Intent(LoginActivityMvp.this, RegisterActivityMvp.class);
                startActivity(intent);
                break;
            case R.id.login_forget_password_tv:

                intent = new Intent(LoginActivityMvp.this, ForgetPasswordActivityMvp.class);
                startActivity(intent);
                break;
            case R.id.login_submit_btn:

                if (!checkOk(mUsernameEt, mPasswordEt)) {
                    t("用户名或者密码为空");
                    return;
                }
                mLoginPresenter.login();
                break;
            default:
                break;
        }
    }

    @Override
    public String getUsername() {
        return mUsernameEt.getText().toString();
    }

    @Override
    public String getPassword() {
        return mPasswordEt.getText().toString();
    }

    @Override
    public void loginSuccessed(UserBean userBean) {
        Intent intent = new Intent(LoginActivityMvp.this, MainActivity.class);
        intent.putExtra(MainActivity.USER, userBean);
        startActivity(intent);
        finish();
    }

    @Override
    public void showToast(String msg) {
        t(msg);
    }
}

其实Model处理数据的时候怎么显示UI的这个问题,本身就问的不太合理,在mvp里面,Model和View是完全隔离的两个部分:

  • Model处理数据时并不关心View该怎么显示
  • 而View层显示时也并不关心Model的处理结果。

真正把Model和View结合起来的就是Presenter。

P

我理解的是调度中心、控制中心,有道翻译说的是任命者,感觉意思也差不多。

经过上面两步,处理数据的Model有了,跟UI交互的View有了,怎么使他们能够合理的协调工作呢?

假设现在在LoginActivity,用户填完账号、密码之后,点击登录按钮,程序校验账号、密码,并根据不同的校验结果让UI做出不同的处理:跳转到主页,或者是给出一个错误的提示。

整个过程大概分为两步:

  • 用户点击登录后,程序校验账号密码:View -> Presenter -> Model
  • 程序返回校验结果,UI做出不同处理:Model -> Presenter -> View

大概流程图如下:

这里写图片描述

LoginPresenter.java

package com.baobeikeji.shower.login;

import com.baobeikeji.shower.bean.UserBean;
import com.baobeikeji.shower.bean.imodel.IUserMode;
import com.baobeikeji.shower.bean.model.UserModel;

/**
 */
public class LoginPresenter {

    private IUserMode mLoginModel;
    private ILoginView mLoginView;

    public LoginPresenter(ILoginView loginView) {
        mLoginView = loginView;
        mLoginModel = new UserModel();
    }

    public void login() {
        String username = mLoginView.getUsername();
        String password = mLoginView.getPassword();

        UserBean userBean = mLoginModel.loadUser(username);
        if (null == userBean) {

            mLoginView.showToast("用户不存在");
        } else if (!userBean.isValidateSuccessed(password)) {

            mLoginView.showToast("答案验证失败");
        } else {

            mLoginView.showToast("验证成功");
            mLoginView.loginSuccessed(userBean);
        }
    }
}

看到这里,是不是很明朗了。

代码示例

Talk is cheap , show me your code。装逼谁不会,拿代码来。

这是整个项目结构,初学mvp时,网上到处都是那个登录注册的demo,我这demo比那个逼格高多了,不仅有登录注册,还有找回密码。(哇,逼格好高啊。。。)

里面有的activity有两个,比如LoginActivity、LoginActivityMvp,前面说了,这个项目一开始不是mvp写的,后来改成mvp了,所以之前的也还保留着,大家可以对照着看。

这里写图片描述

来看看效果图:

登录

注册

找回密码

总结

之前一直纠结是学Mvp还是直接上Mvvm,毕竟习惯了mvc的开发模式,突然接触mvp有种摸不着头脑的感觉,现在想想,感觉其实学习下mvp的这种设计思想也是挺好的。

至于mvc跟mvp孰优孰劣,因为接触的不长,不过也感觉到mvp在解耦上面还是挺好的,这么多年一直讨论设计模式,最终要的一点不就是解耦吗?

毕竟也是菜鸟,有些东西可能理解的不是很完善,希望大家一起来交流。我也会对mvp mvvm继续保持关注的。

源码下载

另外分享一个基于Material Design和MVP的新闻客户端,这是它github的链接

推荐阅读

浅析如何高效的使用MVP

2017-09-24 00:31:56 yang542397 阅读数 2277

浅谈Android中的MVP与动态代理的结合

  • 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

在Android开发平台上接触MVP足足算起来大概已经有一个年头左右。从最开始到现在经历的几个项目中我都采用了MVP架构作为底层框架,使得在view(Activity/Fragment)层中的业务调用逻辑分离到另外的presenter层中,让view层变得非常的轻量,并且不会出现非常复杂的逻辑以及难以阅读和理解的代码块,并且对于编写单元测试用例的实现也是非常的方便和快捷的。


本文主要内容:
1.介绍MVC在Android开发中的使用
2.介绍MVP基本架构
3.介绍MVP在项目A中的使用
4.介绍MVP在项目B中的使用(引入VM对象)
5.介绍MVP在项目C中的使用(引入动态代理以及一级缓存)
6.MVC与MVP的比较
7.不足与回顾
8.未来与展望

一、MVC简介

在讨论MVP之前我想先讨论一下Android传统开发中一直默认使用的MVC架构,还记得当初做的第一次项目就是基于MVC的。

MVC

MVC分为:Model(数据抽象)、View(视图)、Controller(控制器)的三层架构。接下来我们分别来一一解析每一层所对应的职责分别是什么。

  1. View层:对应的则是Android中的layout文件夹中的xml文件,在启动Activity/Fragment的时候,都会加载一个R.layout.xxx的布局文件,使得在视图中显示出我们在xml中定义好的视图。

  2. Controller层:对应的则是Activity/Fragment。当Activity/Fragment加载了layout文件后,我们需要在Activity/Fragment中findViewById(int)去寻找到相对应的view,并对找到的view设置相应的属性以及监听器。而在设置view的属性之前,我们很有可能会先到model中请求一次数据,当数据回调回来后controller就会去更新view了。

  3. Model层:对应的则是一些DataSource以及DataBean的相关对象,这里的DataSource指的是数据的来源。一般数据的来源有2个主要的地方,一个是sqlite,一个是webservice,而我们习惯于将这两种数据的来源封装在一个repository中,对于调用者而言只需要调用repository中的一个获取接口来获取数据,但是这个数据是从内存中还是sqlite还是webservice来,我们都不得而知,从保护了调用实现的逻辑,分解相关的实现,达到调用者的极度简单与简洁,且在单元测试中测试接口也是非常方便的。

我们简单的了解了一下MVC的分层结构后,我们来更加详细的分析一下在Android中,这三层分别是如何相互调用与通信的。

  • 首先是View:Activity_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_hello_mvc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello MVC" />

</FrameLayout>
  • 接下来是Controller:ControllerActivity.java
// Controller
public class ControllerActivity extends Activity {

    private Button mBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.Activity_view);

        // 在此处,controller调用并访问了view
        mBtn = (Button) findViewById(R.id.btn_hello_mvc);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 对于这个 OnClickListener,是属于view的,它是view的监听器
                // 在这里,view直接访问了model
                String btnClickData = ModelDataSource.ins().getBtnClickData();
                Toast.makeText(ControllerActivity.this, btnClickData, Toast.LENGTH_SHORT).show();
            }
        });

        // 在此处controller调用了model
        String btnText = ModelDataSource.ins().getBtnText();

        // 在此处controller设置了view的属性
        mBtn.setText(btnText);
    }
}
  • 最后则是Model:ModelDataSource.java
// Model
public class ModelDataSource {

    private static ModelDataSource mInstance = null;

    public static ModelDataSource ins() {
        if (mInstance == null) {
            synchronized (ModelDataSource.class) {
                if (mInstance == null) {
                    mInstance = new ModelDataSource();
                }
            }
        }
        return mInstance;
    }

    private ModelDataSource() {
    }

    public String getBtnText() {
        // 在这里,
        // 我们可以去数据库中查找数据,
        // 也可以去网络中获取数据
        return "I am from ModelDataSource";
    }

    public String getBtnClickData() {
        // 在这里,
        // 我们可以去数据库中查找数据,
        // 也可以去网络中获取数据
        return "Hello MVC!";
    }
}

在这里,我将model设置为了单例模式,我之所以采用单例,是因为model主要关注的是数据源,而整个模块的数据应该是保证数据的唯一性,这样无论在任何一处修改数据的时候,都可以在每一处都达到数据的统一性,从而保证了数据的安全。就好像一个账号对应的是一份密码一样。而DataBean则是简单的String类型了,我并没有去定义一个数据结构。

从上面的代码中,很显然我们可以很直接的看到View层所表现出的职责是非常的简单的,就是在xml中编写好所需的布局代码,向用户呈现出视图ui,并且响应用户的点击以及各种touch交互事件的响应,其中onClickListener中的onCLick()事件则是view层所响应的处理点,在这个click的响应中view直接调用了model进行数据的获取,拿到数据后并及时的响应。对于view的事件响应和生命周期基本上是依赖于controller进行实现。

而在controller中,它的职责逻辑相对的复杂,它对于view需要将从model中获取而来的数据进行及时的呈现在ui上;而对于model而言controller将会依据app生命周期的变化对model的数据进行及时的刷新和获取,比如当我们接受到一个切换壁纸的广播提醒的时候,此时我们需要在controller中通过调用model来获取新的壁纸数据,然后更新到某处的缓存对象中,再由缓存对象发布出订阅,因为一个app有可能在多个地方需要监听壁纸的变动,例如项目C的icon和locker组件的预览界面,在两个不同的Fragment中需要同时监听壁纸的改变,为了更及时的更新到视图ui上。而在这个demo中,我只是在onCreate(bundle)的时候从model中获取了初始的数据然后更新到btn中并没有做过多通信,但即便如此也可以很直接的看出controller会因为生命周期的变化对model的数据进行良好的CRUD。

最后一个model层很多人会理解为是普通的javabean以及我的大学老师也是这么和我说的,但是我并不这么认为,我不认为model只是很简单的一个数据结构定义,更多的它应该包含大量的数据处理和运算的逻辑,例如从数据库中采集数据的操作或者通过网络请求或者通过NetStream的方法来获取到二进制的数据,接着将这些二进制转换为我们设定好的javabean也就是我们定义好的抽象数据模型,然后该对象进行传递以及显示到视图ui上。具体的model架构逻辑我希望下次在做更加详细讨论。

Demo运行结果:
MVC Demo

至此,我们大概简单的介绍完了MVC接下来我们用时序图的方式来做一个总结:

Created with Raphaël 2.1.2ClientClientViewViewControllerControllerModelModelsetContentView(id:int)init()// find and setClickgetBtnText()加载数据->from sqlite or webservicemBtn.setText(String)点击了按钮onClick()//回调响应getBtnClickData()加载数据->from sqlite or webserviceshowToast(String)

通过时序图我们可以大致的看出整个过程中主要的依赖在与controller,controller不止要处理ui的呈现与事件的响应并且还需要负责和model的通信,且view层也会与model之间通信,三者之间强强关联。

最后我们给出它的优缺点:

  • 优点:Android开发中默认使用的框架,易于上手,能在不需要考虑太多需求的情况下快速开发一些小型demo功能app。
  • 缺点:随着业务的扩展controller会变的越来越臃肿和复杂,大大增加了开发人员的维护成本以及交接成本,使得后期工作难以展开,且随着逻辑的复杂变化以及时间的推移会出现连开发人员自身都对当前代码逻辑的复杂造成错误的理解。

在这里由于是demo所以Controller的代码并不是很多,但是放入项目中假设这是一个非常复杂的view,例如浏览器,它不只要处理顶部的温度天气的ui显示和业务逻辑还要处理底部的数据流以及上下滑动交互搜索等的一切ui显示和业务逻辑,如果将这一切的逻辑和呈现都按照MVC来设计,那在整个Activity中的代码逻辑将是异常的复杂和混乱并且会使得Activity异常臃肿,使得开发难度急剧上升在项目交接过程中也是需要耗费巨大的成本的,同时维护的成本也是巨大的,当然我这里只是假设也许事实并非如此哈。


二、MVP简介

经过了前面对MVC的讨论之后,接下来我们再来讨论一下基于MVP的架构实现方案。

MVP

从上图中我们可以很清晰的看到MVP与MVC中的区别:

  1. 从Controller变成了Presenter
  2. 去除View和Model之间的调用关系,从而彻底的分离了Model和View之间的关联与耦合

MVP和MVC中更具体的区别我们放到后面在做总结与讨论,这里只是大概指出他们两者之间的不同之处。

还是老规矩,我们分别来介绍一下MVP架构中的:Model(数据模型)、View(视图)、Presenter(主持者)他们三者的职责以及相互之间的关系到底是如何运作的。

  1. View层:视图层,它所对应的不只是layout中的xml文件还包括了Activity/Fragment作为视图的显示。这样做是扩大了View层的职责所在,View不仅是设置ui的显示和属性并且还包括了生命周期的回调。

  2. Presenter层:主持者层,它相当于是Controller中的业务逻辑部分,它主要是负责view和model层之间的通信,及时的响应view层的请求并主动的调用model层的数据获取,并且将获取到的数据结果返回给view层中。presenter是另外新建立一个class,并且让view从创建的时候就持有一个presenter的实例,当view发生某些请求响应或者生命周期发生变化,则会迅速的向presenter发起请求,让presenter做出响应的处理,比如:刷新数据、清除数据防止泄露等。

  3. Model层:此处的数据抽象层model和MVC中的model层是一样的,这里就不做更多的叙述。

在MVP的架构中,有一个非常大的特点就是view和model之间的通信必须是通过presenter的传递,也正是因为这种隔离的关系,使得视图和数据之间的关系变得完全分离。当视图改变的时候,数据源部分的代码无需任何变动;而当数据源发生改变的时候,视图部分也根本无需替换。但是事实并非我描述的如此容易,只是在面对整个项目工程的改动来说,我们只需要修改model并且对view层毫无影响,尽管如此工作量依然不容小视。但是如果是使用mvc的默认构建,则会发现整个程序中几乎处处与model耦合,视图或交互的替换基本就是对整个项目的重构,成本是相当大的。

在view和presenter两者之间的通信并不是想怎么调用就可以怎么调用的,他们之间有着一个标准的协议,就是在两者之间定义通用接口IContract,在这个interfac中定义了view层中要暴露的接口也定义了presenter层中需要暴露给view的接口,其目的是利用接口的方式将两者进行隔离,两者之间谁都不认识谁的实现,达到面向接口编程的目的。接下来我们通过代码的形式来一起探索MVP在实际代码中是如何构建的。

  • View和Presenter之间的协议IContract.java
// Contract
public interface IContract {
    interface View {

        void updateBtnText(String s);

        void showToast(String s);
    }

    interface Presenter {

        /**
         * 调用该方法表示presenter被激活了
         */
        void start();

        void loadClickString();

        /**
         * 调用此方法表示presenter要结束了
         * 其目的是为了接触相互持有导致的内存泄露
         */
        void destroy();
    }
}
  • View层ViewActivity.java
// View
public class ViewActivity extends Activity implements IContract.View {

    private Button mBtn;
    private IContract.Presenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.Activity_view);

        // 在最开始的时候构建presenter
        mPresenter = new Presenter(this);

        // View初始化
        mBtn = (Button) findViewById(R.id.btn_hello_mvp);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.loadClickString();
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPresenter.start();
    }

    @Override
    protected void onDestroy() {
        if (mPresenter != null) {
            mPresenter.destroy();
            mPresenter = null;
        }
        super.onDestroy();
    }

    @Override
    public void updateBtnText(String s) {
        mBtn.setText(s);
    }

    @Override
    public void showToast(String s) {
        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
    }
}
  • Presenter层Presenter.java:
// Presenter
class Presenter implements IContract.Presenter {

    private IContract.View mView;

    Presenter(IContract.View view) {
        mView = view;
    }

    @Override
    public void start() {
        String s = ModelDataSource.ins().getBtnText();
        mView.updateBtnText(s);
    }

    @Override
    public void loadClickString() {
        String s = ModelDataSource.ins().getBtnClickData();
        mView.showToast(s);
    }

    @Override
    public void destroy() {
        mView = null;
    }
}
  • Model层ModelDataSource.java:
// Model
public class ModelDataSource {

    private static ModelDataSource mInstance = null;

    public static ModelDataSource ins() {
        if (mInstance == null) {
            synchronized (ModelDataSource.class) {
                if (mInstance == null) {
                    mInstance = new ModelDataSource();
                }
            }
        }
        return mInstance;
    }

    public String getBtnText() {
        // 在这里,
        // 我们可以去数据库中查找数据,
        // 也可以去网络中获取数据
        return "I am from ModelDataSource";
    }

    public String getBtnClickData() {
        // 在这里,
        // 我们可以去数据库中查找数据,
        // 也可以去网络中获取数据
        return "Hello MVP!";
    }
}

ModelDataSource.java的代码是参考MVC中的Model。
Model层中的代码和MVC中Model层的代码基本一致,无非就是改变getBtnClickData()中返回的数据,但这并不会影响到我们对MVP框架的认识。

从代码上看我们可以发现比起传统的MVC从代码数量上看似乎并没有减少反而增加了不少的代码和接口,从逻辑上看似乎有些晕乎。但事实并非如此,当我们理解了MVP后则会发现这种调用方式其实是非常清晰的,因为你根本无需去在乎到底是谁在调用你,你只需要知道:我要让M做什么并且当M做完后我需要将M得出的结果告诉指定的V即可。同时在逻辑上的理解也是非常容易的。

ViewActivity中实现了IContract.View接口,并实现了updateBtnText()showToast()这两个方法,但是这两个方法貌似都没有被调用,只是在onCreate()的时候创建了一个presenter对象,在onStart()的时候调用了presenter.start()方法,然后在onDestroy()的时候调用了presenter.destroy()方法,而当onClick事件响应的时候也调用了presenter.loadClickString()方法,那么既没有回调也没有直接调用,那view中的两个接口方法又是何时被响应的呢?接下来我们将继续分析presenter层的逻辑结构。

Presenter中实现了IContract.Presenter接口并实现了start()\loadClickString()\destroy()方法,在构造方法中有一个view的参数,而这个对象则是view的引用,但是这个view到底是Activity还是Fragment又或者是任意一个接口的具体实现类都有可能,但对于p而言具体的view到底是谁并不知道。presenter和View有一个共同的特点,就是方法之间彼此并不会相互调用而是各自独立的存在。但是值得发现的一点是在start()loadClickString()方法中除了调用model外都调用了view的方法:mView.updateBtnText()mView.showToast(),以此来对view视图的ui呈现以及交互提醒做出相应的响应。而最后的destroy()方法则是用与释放对view的循环引用资源的。

由此我们可以得出一个结论:
对于view来说:

  • 我需要一个主持者,当出现view事件的响应或者生命周期的变化时,我需要告诉这位主持,我要做些什么。
  • 我会提供一系列通用接口,以便于当主持完成我的请求后,调用相应的接口让我明白这件事的结论是如何。
  • 我所有的请求都发给主持,让他帮我做决定,但是这件事的决定是如何做,我并不知道,但我需要结果。

对于presenter来说:

  • 我只会接收到请求后找model寻求帮助,等model做完事情后通知我了,我在把结果传递给view。
  • 我只知道指挥model做事、让view显示数据,但我不干活。
  • 我相当于一座桥,连接着view和model这两座岛,他们谁也不认识谁,想要通信必须要通过我,如果没有我,他们两永远都不会认识。

接下来我们用时序图的方式更加清晰的认识MVP架构之间的调用关系:

Created with Raphaël 2.1.2ClientClientViewViewPresenterPresenterModelModelonCreate() // 初始化onStart() // 生命周期发生变化start() // 激活presentergetBtnText()加载数据->from sqlite or webserviceupdateBtnText()点击了按钮loadClickString()getBtnClickData()加载数据->from sqlite or webserviceshowToast()点击了back建onDestroy()destroy()

很显然,从时序图上我们可以看出其中的调用关系以及调用逻辑非常的清晰,并不会出现任何的跨道调用的现象,程序的执行过程是非常有条理性。
因为有Presenter这个角色的存在使得view部分的代码看上去是非常的清晰的,每一个方法都有它自己的主要倾向和职责所在,彼此之间并不会相互耦合。而Presenter中的代码也是如此,每一个方法都只处理一件事,并不会做其他无相关的事情。

接下来我们再来讨论一下为什么View和Presenter之间需要一个IContract这样的接口角色存在,它存在的意义到底是什么呢?
我们回过头来继续观察ViewActivity.javaPresenter.java这两个类,他们都实现了IContract中的View接口和Presenter接口,而在Presenter中并没有直接对ViewActivity直接持有,而是持有了IContract.View
这样的一个对象;在ViewActivity中也是如此,成员变量的持有类型也是IContract.Presenter。也就是说其实View不一定是Activity,而Presenter也不一定就是Presenter.java,两者只需要是有实现IContract接口中的具体类即可。那么这个时候就有一个非常棒的事情可以做了——是单元测试。

  • 单元测试:此时我们只需要建立一个额外的测试类,让这个测试类实现IContract.View接口,接着再将其传入到Presenter中,此时便自由的测试Presenter中的接口是否有效,是否在回调回我们相应的接口方法,回调方法的时候是否有给出我们想要的结果。而对于View层的单元测试也是如此,构建一个测试类并实现Presenter中所有的接口即可对View中的方法进行大力的测试,看看是否能达到我们想要的预期。

  • 变更逻辑:业务逻辑的变更这个例子在项目B中得到了巨大的便捷与证实。我们都很清晰的明白在项目B中有 联系人、短信、通话记录等备份功能,但是这几个功能中仔细的观察可以发现它们的View层是完全一样的。那面对这样的需求来说,难道要copy几份相同的Activity代码然后通过修改不同的字符串来达到实现界面与功能之间的管理吗?我的给的答案是NO。然而此时的MVP则可以做到共享一个View,却达到不同的业务实现。

    对此我们应该如何来实现呢?

    • 首先构建一个ViewActivity设计好视图UI布局,并实现好IContract中View的接口;
    • 接着分别为 联系人、短息、通话记录等功能构建不同的Presenter对象并都实现IContract中Presenter接口;
    • 然后在View构建Presenter对象的时候,根据外部传递来不同的参数值创建出不同的Presenter;
    • 最后我们的View中的Title文本以及要显示的文案都放在Presenter的start()方法中一一回调给View。

    很显然,通过这种方式,我们并不需要对View做更多的改动,只需要更具不同的业务构建不同逻辑的Presenter给View即可,View则会按照生命周期和事件响应的方式通知给Presenter,而不同的Presenter则会做出不同的逻辑处理,这样就达到了View层的强大复用且在新增备份功能的时候达到开闭原则,只需要增加响相应的Presenter即可,而不是去大量的构建新的Activity或者写很多重复的code。同样的如果是View层发生变化那么我们只需要修改一个地方即可达到修改全部View的视图方案了。这是不是一件让人觉得很有趣的事呢?

    其实针对这类的需求来说,构建多个Presenter并不是最优的解决方案,还有更有趣的方式来实现,但是这里不做过多的讨论。

至此,MVP的基本知识也介绍的差不多了,接下来我们一起来做个优缺点的总结:

  • 优点:
    1. 使用MVP可达到低耦合高内聚并且尽可能的保证了开闭原则,非常符合当前的软件工程;
    2. 由于模块间的耦合很小,可做并行开发,一边开发View,一边开发Model;
    3. 适合大部分的App,代码逻辑清晰易懂,大大降低开发、维护和交接成本;
    4. 视图和底层进行彻底的分离,View发生改变则只需要修改View部分代码,底层数据实现发生改变则只需要修改底层Model的代码。
  • 缺点:对于很小的demo来说构建复杂和麻烦,不适合短期、小型且以后不在做任何维护的模块开发。

MVP在Android开发上虽然是一款非常不错的架构,但它并非万能,并不是所有的APP都适合MVP;与此同时MVP的变种也是非常多,但对于基础的MVP三种角色是必不可少的。Google官方有推出一些关于MVP的Demo:Google MVP Demo 有兴趣可以参考一下


三、项目A中的MVP

回忆起曾开发项目A的日子,依稀的记得那时大概是去年的国庆。在16年9月份的时候我和一位同学交流的时候无意中我们谈起了软件架构,那时候他和我说正在学习MVP,当时的我并不知道MVP到底是什么,对整个软件架构的认知只是停留在对设计模式并不怎么理解并且只知道新建class就开始coding并且完全不知道该何时合适的引入一些经典的设计使得整个软件体系结构变得更加有趣,以至于后面回过头看看自己曾经写的代码竟然是基本都不认识了。。。

先是听到有MVP这么一回事,接着凭借着好奇心我便开始在网络上寻找一些相关的资料。网络上的资料还是非常多的,五花八门,各有个的路子。看了一段时间的资料后,便开始想着寻找一些比较直接的Demo例子来帮助自己理解,于是就在github上看了Google官方的MVP架构tododemo,接着有在自己的电脑上写了一些MVP的相关Demo。

学习的本质是知行合一,大概过了一个月到了10月份,接手了项目A这个项目,在项目开始前我并不打算和之前一样埋头苦干,我希望能加入更多的设计与架构,做的能比以往做的更好,于是我便带着勇气将新学的MVP架构引进了项目A。

项目A中使用的MVP和前面介绍的差不多,是属于基础型MVP其特征主要表现在:

  1. 当Presenter中出现异步获取数据的时候回调回来的数据需要被更新到View上的时候,此时View可能已经消失了。
  2. 当Presenter中在做异步耗时操作的时候,如果View没能及时释放,很大概率的出现context泄露
  3. 极度容易NullPointerException

在Presenter中的大概实现我们以代码的形式来描述:

@Override
public void requestLogin(String id, String pwd) {
    if (TextUtils.isEmpty(id) || TextUtils.isEmpty(pwd)) {
        mView.showToast(R.string.empty);
        return;
    }

    AccountSource.ins().login(id, pwd, new AccountSource.Callback() {

        @Override
        public void success() {
            if (mView == null) {
                return;
            }
            mView.showToast(R.string.login_success);
        }

        @Override
        public void failed() {
            if (mView == null) {
                return;
            }
            mView.showToast(R.string.login_failed);
        }
    });
}

在这里我只是放入了Presenter中的某一个函数的实现,因为View已经其他地方的实现方式是一致的,而Model中的实现在这里并不重要,我们只需要探讨MVP在项目A中的特征应用即可。

由上面的代码可以看出,在Presenter中做异步回调的时候,我们务必判断mView是否还存在,否则就会出现大批量的NullPointerException()。这是一种很简单且基础的方式来处理回调事件,在项目B中这种方案将被新增的一个ViewModel对象替代。


四、项目B中的MVP(引入抽象视图模型)

在项目B中我只要接管的是相册备份,对于相册备份这个功能来说是一个非常具有挑战性的任务,它所需要考虑的不只是图片的上传与下载更多的应该是如何与服务器做好数据相关的同步通信,客户端的数据必须时刻保持最新的状态并且要做到”我“比服务端更早的知道这张图片是否需要备份。

一般在使用MVP的时候我们通常都会为了解决异步回调和context内存泄露做很多功课。在网络上有不少的解决方案例如通过Loader的方式来加载Presenter,但其本质是为了延长Presenter的生命周期,使得Presenter能在View消亡后还持续存在。而本次我介绍的是将视图抽象成模型,使得数据构建在模型上,然后再更新到真实的视图UI中,其目的也是延长了Presenter的生命周期并且解决了Context相关的内存泄露问题。

根据本节重点,我们先构建一个PersonInfo的ViewModel,其代码如下:

public class PersonInfoViewModel {
    String imgUrl; // 头像图片链接
    String name;   // 名字
    boolean sex;   // 性别
    int age;       // 年龄
}

看到这里你可能会觉得奇怪:这不就是普通的bean吗?对,没错这就是普通的bean,但是不同的意图是这个bean中的所有字段都是在视图UI中有一一对应的,如图:

Person Info

由上图我们可以很直接的看到四个字段分别对应四个UI,那么我们只需要做到让Presenter更新数据到VM对象,接着在VM对象中排查View对象是否存在,如果活着就更新到View否则就不更新即可。

如此说来我们只需要以下这3步:

  1. 让VM变为可订阅对象,当VM对象发现改变通知到View更新。
  2. 分离View和Presenter,Presenter的数据只需要更新到VM即可。
  3. 将VM对象与View对象链接。

为了达到我们想要的效果,接下来我们需要重新设计一下VM对象了:

public class PersonInfoViewModel {

    String imgUrl;
    String name;
    boolean sex;
    int age;

    private static PersonInfoViewModel mInstance = null;

    public static PersonInfoViewModel getInstance() {
        if (mInstance == null) {
            synchronized (PersonInfoViewModel.class) {
                if (mInstance == null) {
                    mInstance = new PersonInfoViewModel();
                }
            }
        }
        return mInstance;
    }

    interface IOnDataChange {
        void onChange(PersonInfoViewModel viewModel);
    }

    private IOnDataChange mView;

    public void bind(IOnDataChange view) {
        mView = view;
        notifyDataSetChange();
    }

    public void unbind() {
        mView = null;
    }

    public void notifyDataSetChange() {
        if (mView != null) {
            mView.onChange(this);
        }
    }
}

在这里,我们将视图抽象模型设置为单例模式,因为视图抽象模型毕竟还是装数据的集合,而数据在全局中应该是保证同步和精准的,所以一般情况下是单一的。就好像一个app一般情况下是一份数据库不会同时跑多份数据库来存储相同的数据。

由以上代码中我们可以发现,VM成为了可订阅对象使得在View.onCreate()的时候订阅在View.onDestroy()的时候销毁,即使数据一直延迟回来也不会干扰到View的释放与泄露了。

接着我们再来看看Presenter中的异步实现:

@Override
public void refreshPersonInfo(String token) {
    if (TextUtils.isEmpty(token)) {
        mView.showToast(R.string.empty);
        return;
    }

    AccountSource.ins().presonInfo(token, new AccountSource.Callback() {

        @Override
        public void success(String infoJson) {
            PersonInfoViewModel model = new Gson().fromJson(infoJson, PersonInfoViewModel.class);
            PersonInfoViewModel instance = PersonInfoViewModel.getInstance();
            instance.age = model.age;
            instance.imgUrl = model.imgUrl;
            instance.name = model.name;
            instance.sex = model.sex;
            instance.notifyDataSetChange();
        }

        @Override
        public void failed() {
        }
    });
}

由于和View的分离只需要更新数据,我们就可以很直接很简单的将数据更新到VM中,如果视图有和VM绑定那么一定会同步到View,如果没有则会一直存活在缓存中,等待下次View的bind()事件触发的时候再将数据寄回到View中。

接下来我们再来看看View中的实现是如何的:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    PersonInfoViewModel.getInstance().bind(this);
}

@Override
protected void onDestroy() {
    PersonInfoViewModel.getInstance().unbind();
    super.onDestroy();
}

@Override
public void onChange(PersonInfoViewModel viewModel) {
    mName.setText(viewModel.name);
    mSex.setText(viewModel.sex ? "男" : "女");
    mAge.setText(String.valueOf(viewModel.age));
    Glide.with(this).load(viewModel.imgUrl).into(mImg);
}

这是Activity的代码,我只贴出了部分

这样我们就能在第一时间更新数据到UI上了,是不是比之前更加的灵活呢?
但这依然是存在缺陷与不足的:

  • 如果要想在Presenter中通知View弹出Toast,Dialog时,该怎么办?
  • 如果我们当前这个View是一个非常复杂的View,那onChange()方法岂不是变得异常复杂了?
  • 如果我们只想更新局部的View数据,那该怎么办?

等等一堆的问题让我又从MVPVM倒回了MVP,Presenter依然需要持有View,但不是真的View而是一个假货。具体该如何实现呢?这也是我们需要在下一节 项目C中的MVP 中所要讨论的方法了。


五、项目C中的MVP(引入视图代理+一级缓存方法)

项目C是我接手的大概算是第5个项目了。在我刚接触到项目C的代码时,我发现项目C的代码竟然也是跑MVP的,我顿时刚到无比的喜悦与惊喜。但待我深入查看后发现虽然使用了MVP但是却依然是停留在最初的阶段。不过这并没有什么,我决心将其改变使得整体的架构变得更加有趣。在项目C中引入了RxJava2和Retrofit2,这两项优秀的开源库是我一直渴望学习和使用的,但遗憾的是一直未能找到时间。刚好此次项目C引入了这两项库的结合使用,使得我更加的兴奋与激动。

接下来在讨论本节内容前,需要先了解一些相关知识:

  1. Java动态代理:什么是Java 动态代理?
  2. 运行时注解:Java中的注解是如何工作的?

动态代理现在Android开发中广泛的被使用着,包括插件化、retrofit2等新技术都会使用着动态代理。通过动态代理的方式,我们可以在对业务完全透明的情况下去修改方法的执行过程。在这里我们就不做更多的讨论了。

本节在MVP中引入的视图代理和一级缓存方法都是依赖于动态代理实现。原本是通过静态代理实现,但是在实践的过程中我们很快就发现静态代理是符合OOP设计的,但是代码冗余太厉害且太过于繁琐。于是我们发现了动态代理机制。动态代理这种实现方案更像是AOP编程设计。它倾向并非横向于某种对象而是垂直于某种业务角度。举个例子:小明要削一个苹果给小红吃。我们可以苹果为对象进行削这个方法,然后就能得到一个削好的苹果。而AOP却是你不需要知道苹果这个对象,你可以直接把苹果传递给小红,但是在传递的过程中,我们可以针对这个传递的方法来判断我们当前的业务是否需要进行削,如果要则削皮,不要则不削。同样,即使你传递的是梨,我们也可以通过同样的方式来确定是否要削。这就像埋点,我们需要在onClick()的方法实现中记录埋点,但是我们也可以通过记录onClick()方法触发的时候进行埋点记录,这样就不用在每个onClick()的方法中实现埋点了。

好了题外话不多说,我们进入正题。对于引入视图代理和一级缓存在MVP中,首先Presenter是不改变的,它和最初的样子是一样的依然都是持有一个mView对象,但是持有的并不是真正的View。而现在的mView呢也依然是最初的mView,不做任何改变。唯一改变的就是我们新引入了一个代理的对象,现在我们来看看这个代理对象是如何实现的:

public abstract class AbstractViewCacheProxy<T extends IView> implements InvocationHandler {

    /* 如果是weakhashmap。Fragment destroy view就会回收数据了 */
    private final Map<Method, Object[]> mViewCaches = new HashMap<>();
    private WeakReference<T> mView;

    public T proxy(Class<T> viewClass) {
        if (viewClass == null) {
            throw new NullPointerException("Proxy class is NULL, vmProxy is NULL!");
        }
        return (T) Proxy.newProxyInstance(viewClass.getClassLoader(), new Class[]{viewClass}, this);
    }

    void bind(T view) {
        if (view == null) {
            return;
        }
        unBind();
        mView = new WeakReference<>(view);

        for (Method method : mViewCaches.keySet()) {
            invokeMethod(view, method, mViewCaches.get(method));
            LogHelperUtil.i("AbstractViewCacheProxy-bind: ", method.getName());
        }

        view.bindProxyFinish();
    }

    void unBind() {
        if (mView != null) {
            mView.clear();
            mView = null;
        }
    }

    boolean isBind() {
        return mView != null && mView.get() != null;
    }

    void destroy() {
        unBind();
        mViewCaches.clear();
        onDestroy();
    }

    /* 请在此处释放和清理资源 */
    protected abstract void onDestroy();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (isCacheMethod(method)) {
            mViewCaches.put(method, args);
        }

        if (mView != null && mView.get() != null) {
            return invokeMethod(mView.get(), method, args);
        }
        return null;
    }

    private boolean isCacheMethod(Method method) {
        CacheMethod cacheMethod = method.getAnnotation(CacheMethod.class);
        return cacheMethod != null && cacheMethod.isCached();
    }

    private Object invokeMethod(Object view, Method method, Object[] args) {
        if (view == null || method == null) {
            return null;
        }
        try {
            return method.invoke(view, args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}

由以上代码可以看出,这是一个InvocationHandler对象,这个对象主要是在生成代理的时候需要传入,在调用代理对象方法的时候会调用该对象子类中的invoke(Object proxy, Method method, Object[] args);方法,而我们便可以在该方法中做我们想做的事。

本类中的字段说明:

private final Map<Method, Object[]> mViewCaches = new HashMap<>();
private WeakReference<T> mView;

这两个字段中:

  • mViewCaches用来存储调用的方法和调用的参数
  • mView则是真实的View对象,是一个弱引用对象。

本类中的一些重点方法说明:

  1. 首先是public T proxy(Class<T> viewClass)方法,该方法主要是通过Proxy.newProxyInstance()构建一个动态代理的对象,而这个代理对象的InvocationHandler就是本类,而我们所代理的class就是我们的IContract.View.class。
  2. void bind(T view)void unBind()方法,其目的和之前所提到VM中的bind()unbind()是一样的,就是为了将真实的View提供到代理对象中,只是这里是代理对象而之前是一个VM对象。但是在这里的bind()方法不只是简单的绑定一个View,它还做了一个事情就是将之前调用过的方法和参数通过method.invoke(view, args)的方法传递给最新绑定的View,这样是不是就相当于我们跑了一遍Presenter中请求的数据,然后将数据返回给View中呢?而不同的是,我们所提供给View中的数据是缓存且最新的缓存。
  3. public Object invoke(Object proxy, Method method, Object[] args)这个方法主要是在当我们的代理对象的任一方法被调用的时候,则会回调此方法。
  4. private boolean isCacheMethod(Method method)这个方法是用于获取当前代理对象调用的方法是否需要被缓存,其理论是通过获取动态注解来判断是否需要被缓存。

看到这里也许聪明的你可能大致就已经明白了我的意图。所谓视图代理对象的实现其实主要是通过动态代理的方式生成一个代理类,当视图代理的方法被主动调用的时候我们通过运行时注解来判断此方法是否需要被记录,如果要则存入mViewCaches后调用,如果不要则直接调用该方法即可。不论View是否有效,我们的视图代理对象方法都会被成功的执行,但是会不会具体的落实到真实的View中就不一定了。即使这次不会落实在View中也不怕,在View下一次绑定我们的视图代理对象的时候,我们依然会在bind()方法中回调之前的调用记录,将最新的缓存数据传递给View,这样做既可以保证数据更新的及时,也可以保证每一次请求的有效性且有价值,因为它基本一定会被应用到View中,而不会因为View的离开而导致本次请求的数据浪费了。

接下来我们再来看看View中是如何与ViewProxy(视图代理)进行链接的:

public class FontDetailView extends AbstractFragmentView<IFontDetail.Presenter, FontDetailViewProxy>
        implements IFontDetail.View {

    private FontDetailLoopPagerAdapter mAdapter;
    private AlertDialog mInDataDialog = null;
    private ViewHolder mViewHolder;

    @Override
    protected FontDetailViewProxy onCreateViewProxy() {
        return new FontDetailViewProxy();
    }

    @Override
    protected FontDetailPresenter onCreatePresenter(FontDetailViewProxy viewProxy) {
        return new FontDetailPresenter(viewProxy.proxy(IFontDetail.View.class));
    }
    ...
}

这是项目C中FontDetailView.java的部分代码

上面的代码是继承Fragment的,因为它需要活在ViewPager中。在这里,我贴出的是具体实现的View而不是抽象的View,其目的是为了能够更好的解析框架的实现与使用。

我们可以很直观的看到两个方法:onCreateViewProxy()onCreatePresenter(FontDetailViewProxy viewProxy),这两个方法会在Fragment的构造方法中调用。在FontDetailViewsuper的构造方法中我们先是调用了onCreateViewProxy()方法来创建一个具体的FontDetailViewProxy对象,该视图代理对象是AbstractViewCacheProxy的实现子类,接着会将创建好的ViewProxy对象作为参数传递给onCreatePresenter(ViewProxy)中,然后在onCreatePresenter(ViewProxy)方法中我们构建了一个Presenter,并且我们通过ViewProxy.proxy(IContract.View.class)的方式创建了一个代理对象传递给了Presenter,而这个代理对象的InvocationHandler就是我们所创建的FontDetailViewProxy对象了。

经过这两个方法的调用,View和Proxy之间的对象就构建完成了,在抽象的View中会在onActivityCreated()的时候自动将自身绑定到ViewProxy中,具体的代码我就描述了就是调用了bind()方法罢了。

好了,在View中创建了ViewProxy并且将ViewProxy传递给Presenter接着将View和ViewProxy两者进行绑定的步骤以及实现我们已经大致了解了,接下来我们将要讨论如何对制定的调用方法及其参数进行缓存了。

讨论调用方法的缓存其实就是在讨论ViewProxy中的isCacheMethod(Method)方法了,让我们再一次回顾该方法的代码实现:

private boolean isCacheMethod(Method method) {
    CacheMethod cacheMethod = method.getAnnotation(CacheMethod.class);
    return cacheMethod != null && cacheMethod.isCached();
}

代码很简单,就是从method方法对象中获取指定的CacheMethod注解,如果获取到了并且isCached()方法为true则返回真,否则返回假。那么也就是说,其实我们只需要在需要缓存的方法前加上@CacheMethod这个注解,则该方法对象以及方法的调用参数就会被缓存了。接下来我们一起来看看CacheMethod这个注解的代码:

public @interface CacheMethod {
    boolean isCached() default true;
}

注解的代码非常的简单,就是单纯的一个isCached()方法且默认值为true。我们再来看看该注解的使用:

  • IFontDetail.java
public interface IFontDetail {
   interface View {
       ...
       @CacheMethod
       void updateBtnText(int resId);
       ...
   }
   interface Presenter {
       ...
   }
}

由于不相关的接口过多,这里进行了省略只显示出了需要分析的方法

由代码中我们可以看出,使用注解的方式非常的简单,只需要在你想要缓存的方法前加上这个注解即可,接着MVP中的Proxy则会自动将该方法进行缓存和记录,等到再次bind()的时候则会回调此方法。

至此,MVP+Proxy+Cache的介绍基本可以落下帷幕了,但是依然有一些细节是需要注意的:

  • bind()方法会被调用几次?如果只是一次那缓存还有何用?

    bind()方法只会在onActivityCreated()方法中被调用,也就是说onActivityCreated()方法被执行了几次bind()方法就会被调用几次,而在onDestroyView()的时候会调用unbind()方法进行接触绑定。

    假设 1.在一个Activity中只有一个Fragment,如果一直保持在前台那么bind()只会被调用一次,这个方法缓存意义不大,但是如果当前Activity被放在后台了系统调用了当前Fragment的onDestroyView()方法但是没有调用onDestroy()方法,那么当这个Fragment再次显示的时候方法缓存的功效就很明显了,它会在最快的时间内恢复视图在销毁前的状态。

    假设 2.在一个Activity中有多个Fragment并用ViewPager来组合,比如项目C的首页则是4个Fragment的组合了。此时ViewPager的adapter是一个FragmentPagerAdapter,在这个Adapter的内部会缓存Fragment。当我们将ViewPager从第一页滑动到第三页的时候,此时第一页的onDestroyView()方法则会被调用,当我们滑动到第二页或者第一页的时候则会调用Fragment的onCreateView()-onActivityCreated()此时利用方法缓存的形式就可以在最快速度且不需要做任何网络以及数据库请求的情况下恢复Fragment在销毁前的状态。

  • CacheMethod能活多久?它在何时清除?

    方法的缓存是在Presenter首次调用指定的缓存方法的时候开始进行缓存的,而缓存的数据就会在Activity.onDestroy()时被全部清空。代理对象的生命周期和View的生命周期是一样的,当View被彻底的Destroy掉后,代理对象也会跟着一起销亡。

可能还会有更多的问题是值得探讨的,但本节的内容也差不多结束了,最后我们再用时序图的方式来对本节内容的执行过程做一个演示以及章节总结:

Created with Raphaël 2.1.2ClientClientViewViewViewProxyViewProxyProxyProxyPresenterPresenterModelModelViewProxy()Proxy.new(View.class)return Proxy ViewPresenter(Proxy View)startLoadingData()loadData()from remote or sqliteupdateBtnText(int)invoke(Method,args)isCacheMethod()View is existupdateBtnText(int)onDestroy()Clear Method CacheonDestroy()

上面的时序图只是简单的描述了一下彼此之间的通信过程,其中ViewProxy和Proxy可以看做是同一个对象,只是被分解为了两个部分。

总结:在MVP中通过动态代理的模式将Presenter和View之间进行解耦,解决了很多之前使用VM时所引发的问题,比如在面对复杂视图的时候我们该如何解析VM呢?而在Proxy的方式中,我们无需考虑此问题,因为Proxy是面向调用方法进行缓存而并非对象缓存,被调用者发生了什么事件Proxy则记录什么事件,等被调用者回来后Proxy再还给他就是了。对于局部刷新也是如此。对于显示Toast和dialog等方法,我们也可以正常的使用,对于这类方法我们无需缓存,因为只有当View活着的时候,我们才有必要去显示这些东西,当View离开后一般情况下是不需要显示这类视图的,如果有特定的需求那可做特别的处理。

最后我们再来探讨一下它的优缺点:

  • 优点

    • 对于一个Activity对应多个Fragment的情况下使用代理和缓存模式是非常可靠和有效的,并且保证了每一次请求的数据的有效性,而不是当View一解除就deprecate掉。
    • 对于方法的缓存可控性高,有需要则缓存,无需要则不缓存。
    • 在不影响不改变Presenter的情况下,解除Presenter与View之间的循环引用,完美解决Context内存泄露。
    • 架构逻辑清晰,项目交接容易
  • 缺点

    • 架构逻辑需要发费一定的学习成本。
    • 使用简单但是不适合一次性的功能Demo。

六、MVC与MVP之间的比较

经过了前面几节的内容讲解,我们大概能明白MVP和MVC架构的大致实现和思想方式了。而面对这两种架构方案,我们一起从几个维度来对他们做一个简单的比较。

从上来看:

\ MVC MVP
开发速度 快,在项目最开始的时候不需要考虑过多的扩展和代码间的耦合,甚至无需考虑任何架构问题,可以直接拿起键盘敲,适用于Demo功能预研,如果代码过多到后期基本无法维护,交接困哪维护成本巨大,开发效率从高到低,开发质量也会越来越不如最开始。 在项目最初期需要先做底层架构,接着让每个模块都按照架构规则去实现不同层的实现,并使之相互关联且低耦合、高内聚,适用于正式项目的开发。在项目后期开发速度和最开始的前期一样,交接速度快,开发新功能成本跟一开始差不多,保持平衡状态。
代码可读性 在项目初期代码量少,阅读代码就像是读文章一样,如果a则b否则c,所有的逻辑都在一个方法中按照流程来体现,就像面向流程一般的开发。但是在项目后期代码量多且复杂的时候,按照这种流程式阅读只会越读越晕,到最后找不着头脑。 在项目最初,有效且明显的分解出每一层的单一职责,并且以OOP的方式进行开发,在View层不需要Care逻辑,我只管视图UI的展示;在Presenter层我们无需在乎如何显示也无需在乎数据如何采集,只需要保证业务逻辑的清晰即可,对请求及时响应,对回调及时处理即可;在Model层我们只需要在乎数据如何采集,到底是sqlite还是remote,到最后只需要将采集到的数据送回即可,但是数据如何用我们不在乎。
代码复用 采用MVC对代码复用的可能性极低,View基本不能复用,在Controller更是不可能,最多只能在Model层上复用几个数据采集方式。 View可做大量复用,例如在项目B中的备份界面,从联系人进入和从短信进入是共用同一界面,只需要替换Presenter即可达到业务逻辑的变更。复用能力强且分离能力更强。
MVC和MVP之间还有很多的不同在这里我们就不再一一列举了,在实践中会有跟多的体会。

由以上的表格我们可以很明显的看出两者之间的差异,各有各的优势。至于在实际的开发中到底要使用MVC还是MVP这该由开发者自己定义了。


七、不足与回顾

不知不觉已经到了本文的尾声,接下来我们来对本文做一个简单的回顾:

  • 在文章的开始我们讨论了在Android中传统的开发模式MVC,并且对其进行了基本的介绍,并通过部分的代码进行事例的讲解,最后以时序图的方式结束了本节的内容。

  • 接着我们讨论了MVP的基础内容,同样也给出了部分code的方式来进行实例的讲解,最后也给出了层次间的时序图,并且提出了一些优缺点。

  • 在来就是讲解MVP分别在曾经的项目A、项目B和项目C中的使用已经MVP中的阶梯式的扩展与进阶型的讨论,分别以基础-视图-视图代理的方式来对MVP进行不同的改造,于此同时我们也给出了不少的code碎片进行更深入的讲解。

  • 最后就是已表格的方式对MVC和MVP之间进行一个非常简单的比较。

虽然MVP是一款非常优秀的架构,但是再优秀的架构也还是会有缺陷的。在技术的世界中,没有最完美的架构,只有最符合需求的架构,尽管再优秀在完美的架构也还是会有很多不足之处的。在MVP中也是存在不少的不足之处,例如:在构建View和Presenter的时候,我们需要多写大量的冗余接口,这无非是增加了额外的代码量。还有就是假设我需要新增方法或者修改某个方法的参数、返回值等,则至少需要变动3个以上的文件,View,Presenter,以及IContract接口。这些都是MVP的不足之处。但是往往我们不能因为某些可以容忍的不足而放弃,也许放弃可以加快眼前的步伐,但对于未来将深陷到难以自拔了。


八、未来与展望

在当下Android开发的技术中已经有了非常多的Android开发架构方案,除了MVC、MVP以外还有MVVM、Clean、Flux以及Google最新推出的Lifecycle+ViewMode+Repository的架构方案。无论是那种架构,他们都拥有自己的特点,适用于不同的需求定制,各有各的好与坏。

在MVP的架构方案上,网络上也有非常多的变种与发展,我提出的MVP+Proxy+Cache的方案在目前看来尚未在项目中产生问题,但是我深知这是远远不够的,例如:在Cache的时候其实我们是否应该加入一些过期的机制,或者是否何时需要再次强制拉取更新数据等等,不少的疑问还需要做更多的探索与发现,而这些探索将会伴随着未来更多变化的需求来进行改变和提升的。

在未来的日子中,我们将会更加努力的提升当下的架构水平与开发能力,在现有的基础上做出更大的突破与进步,使得开发和迭代的速度更加迅速,并且尽可能的努力让眼前的项目保持一个更加强有力的健壮性,这也一直都会是我们不断追逐的目标。

最后还是那句话:世界上没有什么最好的架构,只有最符合需求的架构。

github:https://github.com/gpyAngyoujun/MVPDemo


END

2017-05-11 21:24:09 qq_34379015 阅读数 382

SmallExcellent

一个使用了mvp+bmob+高德地图sdk+litePal的兼职工作平台项目 起初对mvp不是很了解以至于,刚开始写的时候比较迷糊,但是 熟能生巧,慢慢的写起来,对mvp的理解越来越深刻,到现在也是 慢慢明白了其逻辑与代码分离的设计思路,后台处理主要是使用Bmob,Bmob是个人开发者后台的得力 助手,项目的数据主要存储在此。项目地图定位等模块都是借用了 高德开发平台所提供的api。顺便还使用了郭霖第一行代码第二版中 提到的litePal,将此开源,大家共同学习,勿用做其他目的。

*登录模块

img

*注册模块

img

*主界面模块

img

*路线规划模块

img

*兼职查询模块

img

*个人信息模块

img

*工资单模块

img

*兼职记录模块

img

*足迹模块

img

*个人简历模块

img

*编辑简历模块

img

*设置模块

img

*兼职详情模块

img

GitHub地址

2016-10-15 16:02:02 qq_23547831 阅读数 3623

转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了关于Android开发过程中常见的内存泄露场景与检测方案。Android系统为每个应用程序分配的内存是有限的,当一个应用中产生的内存泄漏的情况比较多时,这就会导致应用所需要的内存超过这个系统分配的内存限额,进而造成了内存溢出而导致应用崩溃。在实际的开发过程中我们由于对程序代码的不当操作随时都有可能造成内存泄露。具体更多关于Android常见内存泄露与检测的内容可参考我的上篇文章。

本文我们将讲解Android开发中常常涉及到的MVC/MVP/MVVM等模式的基本概念。许多童鞋对Android开发中涉及到的MVC、MVP、MVVM这三种模式不是太清楚,我认为无论是MVC、MVP亦或者是MVVM都是一种代码组织方式,通过这种代码组织方式能够让代码更有层次感,各个层次主要负责各自的工作,这样降低了整个项目的代码逻辑耦合度与可读性。

下面对MVC、MVP、MVVM等设计模式逐一的做一下说明:

MVC开发模式:

MVC,即Model层,View层,Control层,在JAVAEE中MVC是一种经典的开发模型,下面是引用的一段对其的说明:

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,来控制V层和M层通信以此来达到分离视图显示和业务逻辑层。

简单来讲就是:

  • 视图(View):用户界面

  • 控制器(Controller):业务逻辑

  • 模型(Model):数据保存

MVC它的数据流转:

这里写图片描述
(盗用了一下网上的图…)

  • 用户操作界面,View接受指令,View 传送指令到 Controller,也就是从View层到Control层的箭头所示

  • Controller 完成业务逻辑后,要求 Model 改变状态,也就是从Control层到Model层的箭头所示

  • Model 将新的数据发送到 View,用户得到反馈,也就是从Model层到View层的箭头所示

Android中MVC模式的体现:

其实Android开发的主要流程都是MVC模式的,比如我们常见的Activity+Layout+Model展示业务逻辑的模式,其中:

Activity - 对应着Controller层,主要是控制层,用于实现业务逻辑

Layout - 对应着View层,主要用于展示页面

Model - 对应着Model层,主要是保存数据

MVC模式的优势:

  • 使用MVC模式降低了程序中的耦合度,使应用程序视图层与Model层分离,减少了代码之间的相互影响;

  • 由于使用MVC模式降低代码耦合度,因此可以很方便的扩展现有程序;

  • 不同代码模块职责划分明确,有利于代码的维护与升级;

MVP开发模式:

MVP开发模式是MVC模式一种进阶,MVP和MVC模型的主要区别是model层与View层不再发生关系而是通过Presenter层作为中间的枢纽。并且各个部分之间都是双向关联的;

简单来讲就是:

  • 视图(View):用户界面

  • 控制层(Presenter):业务逻辑(负责与View层和Model层双向交互)

  • 模型(Model):数据保存

MVP它的具体数据流转是这样的:

这里写图片描述
(盗用了一下网上的图…)

  • 用户操作界面,View接收指令,View传送指令到Presenter层,也就是从View层到Presenter层的箭头所示

  • Presenter完成业务逻辑后,要求Model改变状态,也就是从Presenter层到Model层的箭头所示

  • Model状态改变之后将结果返回给Presenter层,然后Presenter层在将结果反馈到View层,也就是从Model层到Presenter层,从Presenter层到View层的箭头所示

在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变。

MVP模式的优势:

  • MPV开发模式与MVC开发模式有的优势相似,都是降低了代码的耦合度

  • 使用MVP模式View层与Model层不在相互关联,可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部

MVVM它的具体数据流转是这样的:

MVVM与MVP是相类似的,唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

简单来讲就是:

  • 视图(View):用户界面

  • 控制层(VM):业务逻辑(负责与View层和Model层双向交互)

  • 模型(Model):数据保存

这里写图片描述
(盗用了一下网上的图…)

  • 用户操作界面,View接收指令,View传送指令到Presenter层

  • ViewModel完成业务,改变Model层数据

  • Model状态改变之后将结果返回该ViewModel层,然后ViewModel层自动更新View层显示

注:google提供的官方data binding框架采用的就是MVVM模型,关于databinding框架的相关知识可参考:完全掌握Android Data Binding

参考文章:
MVVM大话开篇
MVC,MVP 和 MVVM 的图示


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
Android产品研发(十五)–>内存对象序列化
Android产品研发(十六)–>开发者选项
Android产品研发(十七)–>Hybrid开发
Android产品研发(十八)–>webview问题集锦
Android产品研发(十九)–>Android studio中的单元测试
Android产品研发(二十)–>代码Review
Android产品研发(二十一)–>Android中的UI优化
Android产品研发(二十二)–>Android实用调试技巧
Android产品研发(二十三)–>Android中保存静态秘钥实践
Android产品研发(二十四)–>内存泄露场景与检测


本文以同步至github中:https://github.com/yipianfengye/AndroidProject,欢迎star和follow


2016-05-13 14:10:20 a910626 阅读数 927

Q:最近看到很多文章都在说MVP怎么怎么好,还有MVVM啥的
最近看到很多文章都在说MVP怎么怎么好,还有MVVM啥的,请问你认为哪个比较适合android,而你现在采用的是哪些模式呢?
【郭霖】A:郭霖
对于这个问题,我还是有点心得的。像MVP这种架构模式,肯定是有它的意义的,它提供了一种设计规范,让我们能把业务逻辑从Activity中提取出来,让代码看起来更工整,这里先给个肯定。但至于用不用那就纯粹是看个人了,比如我自己就是不用的。对于架构模式这种东西没必要迷信,好像大家都说这个好,那我也必须要用这个,因为代码永远也是写不到最好的,如果你觉得你自己完全能够管理好Activity中的业务逻辑而不会混乱不堪的话,那也完全可以不用MVP模式,在MVP出现之前仍然有需要出色的代码架构,这些都是靠工程师自己的思路创造出来的,而不是按照一个模式循规蹈矩写出来的。在我看来,MVP引入了大量的Presenter这点就做得不够优雅,并且去年Android全球开发者大会谈项目架构时,Google工程师也没有推荐使用MVP,只是提到了一下有这个东西,但话锋一转:今天没准备讲它。如果问我我是使用什么模式来设计架构的话,那我的回答就是,我没有任何模式。但我问遵守一个原则,就是DRY(Don’t Repeat Yourself),当你把同样一段代码写两遍的时候,就是你需要思考去重构的时候,我觉得这样写下来的代码架构同样非常优雅,大家可以试试。

Q:《第一行代码》之后有什么推荐书籍吗?
【郭霖】A:可以看《Android开发艺术探索》这本书,这本书写得挺好的。不过我给你一个建议,最好的学习方式就是做项目,以项目驱动的方式进行学习。当你需要实现一个功能的时候,你需要学习各种相关的技术来完成它,其实在这个过程中你就已经在慢慢成长。如果只看书不练习的话,一是理解层次可能会比较浅,二是学完要不了多久你就又会忘记了。

Q:如何提升自己的编程水平
应该有很多跟我一样的老菜鸟,做android也做了3年甚至更多 但是水平总是跟以前入门一样,到处找点代码贴贴代码。不知道怎么才能有质的提高 希望大神给出宝贵意见
【郭霖】A:我也见过很多你这样的同学,他们做编程有段时间了,但是更深点的东西都不会。或者说搜索能力可以,但是自己学不出来东
西。我给给他们的建议就是先找些小的轮子造一造(可以是重复的轮子,重要的是自己亲自码代码,不是光看)。小轮子造完
了就造大的。当然刚开始的时候你可能会觉得无从下手,感觉小轮子也造不了,那就照着别人的优秀开源项目的片段功能或
模块抄一抄。熟能生巧,时间长了自己就提高了很多,也会自己写出东西了。温馨提示:一定要自己亲手多代码。

Q:你是如何如何快速学习提高技术水平的?
[罗迪]A:有句话说的好: Read the f**k source code。在一项语言基础扎实的情况下,去学习具体平台的开发,绝佳的方式就是阅读优秀的源码。无论代码做得是什么,优秀的代码都会在不经意间让你有所感悟。现在,网上各种文章介绍着各种各样的设计模式。尽管你可能看懂了它的组织形式,却不一定能够融会贯通。阅读源码的过程,你能够真切的体会到一个设计模式的妙处。Google开源了Android这个珍贵的宝藏,阅读它的源码成为了我提高技术水平的方式。Android虽然为开发者提供了详细的文档,但是如果仅仅止步于SDK层,很多的问题你都会有”知道怎么处理,但是不知道原因”的感觉。我敬佩思想的创造人,因为有了思想,轮子可以再造,但是如果没有思想,轮子是不可能造出来的。

读再多的书,不如坐下来写两行代码有成效。编程是实践科学。实践是首要的,其次才是阅读。

读项目源码是一种非常好的学习方式.

没有更多推荐了,返回首页