MVP模式总结

这篇文章是我学习MVP模式时,总结网上多篇文章的结果。内容和图片大部分来自其他文章。如果侵犯了您的权益,请告知,我会及时修改、删除。

1 介绍

1.1 背景

  MVP,全称 Model-View-Presenter,要说MVP那就不得不说一说它的前辈——MVC。
  MVC(Model-View-Controller,模型-视图-控制器)模式是80年代Smalltalk-80出现的一种软件设计模式,后来得到了广泛的应用。MVP(Model-View-Presenter,模型-视图-表示器)模式则是由IBM开发出来的一个针对C++和Java的编程模型,大概出现于2000年,是MVC模式的一个变种。
  对比:


对比.png

  MVC与MVP的基本思想有相通的地方:Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。
  MVC的Controller用于更新UI界面和数据实例。
  MVP的Presenter是View与Model交互的中间纽带。
  MVC主要目的是将Model和View的实现代码分离,从而使同一个程序可以使用不同的表现形式,Controller存在的目的则是确保Model和View的同步,一旦Model改变,View应该同步更新。例如:View层接受用户的输入,然后通过Controller修改对应的Model实例。同时,当Model实例的数据发生变化的时候需要修改UI界面,可以通过Controller更新界面。(View层也可以直接更新Model实例的数据,而不用每次都通过Controller,这样对于一些简单的数据更新工作会变得方便许多)。
  MVP主要目的是隔离UI、逻辑(UI逻辑和业务逻辑)、数据。在MVP中View并不直接使用Model,它们之间的通信是通过Presenter来进行的,所有的交互都发生在Presenter内部。
  MVP与MVC的区别:MVC中是允许Model和View进行交互的。而MVP中很明显,Model与View之间的交互由Presenter完成,还有一点就是Presenter与View之间的交互是通过接口的。

1.2 为什么用MVP?

  Android的整个代码架构就是一个MVC。
  Model:模型层,相当于JavaBean和我们的数据请求代码。
  View:视图层,对应Android的layout.xml布局文件。
  Controller:控制层,对应于Activity中对于UI 的各种操作。
  看起来MVC架构很清晰,但是实际的开发中,请求的业务代码往往被丢到了Activity里面,Android中的layout.xml布局文件只能提供默认的UI设置,所以开发中视图层的变化以及操作也被丢到了Activity里面,再加上Activity本身承担着控制层的责任。所以Activity达成了MVC集合的成就,最终我们的Activity就变得越来越难看,代码从几百行变成了几千行,维护的成本也越来越高。
  MVP与MVC最大的不同,其实是Activity职责的变化,由原来的Controller 变成了 View,不再管控制层的问题,只管如何去显示。控制层的角色就由我们的新人 Presenter来担当,这种架构就解决了Activity过度耦合控制层和视图层的问题。

2 实现

  MVP更多的是一种思想,而不是一种模式,每个开发者都可以按照自己的思路来实现具有个性化的MVP,所以不同的人写出的MVP可能会有一些差别。下面给大家介绍一下MVP不同的实现方式。

2.1 入门

2.1.1 方式一

  存取用户信息的 MVP 小 Demo:可以从EditText读取用户信息并存取,也可以根据ID来从后台读出用户信息并显示。
  原文:MVP模式在Android开发中的应用

2.1.1.1 界面:

界面.png

2.1.1.2 目录结构:

目录结构.png

2.1.1.3 代码:

  • 首先我们需要一个UserBean,用来保存用户信息:
public class UserBean {
    private String mFirstName;
    private String mLastName;
    public UserBean (String firstName, String lastName) {
        this.mFirstName = firstName;
        this.mLastName = lastName;
    }
    public String getFirstName() {
        return mFirstName;
    }
    public String getLastName() {
        return mLastName;
    }
}
  • 再来看看View接口,根据需求可知,View可以对ID、FirstName、LastName这三个EditText进行读操作,对FirstName和LastName进行写操作,由此定义IUserView接口:
public interface IUserView {
    int getID();
    String getFristName();
    String getLastName();
    void setFirstName (String firstName);
    void setLastName (String lastName);
}
  • Model接口。同样,Model也需要对这三个字段进行读写操作,并存储在某个载体内(这不是我们所关心的,可以存在内存、文件、数据库或者远程服务器,但对于Presenter及View无影响),定义IUserModel接口:
public interface IUserModel {
    void setID (int id);
    void setFirstName (String firstName);
    void setLastName (String lastName);
    int getID();
    UserBean load (int id);//通过id读取user信息,返回一个 UserBean
}
  • Presenter。至此,Presenter就能通过接口与View及Model进行交互了:
public class UserPresenter {
    private IUserView mUserView;
    private IUserModel mUserModel;

    public UserPresenter(IUserView view) {
        mUserView = view;
        mUserModel = new UserModel();
    }

    public void saveUser(int id, String firstName, String lastName) {
        mUserModel.setID(id);
        mUserModel.setFirstName(firstName);
        mUserModel.setLastName(lastName);
    }

    public void loadUser(int id) {
        UserBean user = mUserModel.load(id);
        mUserrView.setFirstName(user.getFirstName());//通过调用IUserView的方法来更新显示
        mUserView.setLastName(user.getLastName());
    }
}
  • UserActivity。 UserActivity实现了IUserView及View.OnClickListener接口,同时有一个UserPresenter成员变量:
public class UserActivity extends Activity implements OnClickListener,
        IUserView {
    private UserPresenter mUserPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
   
        mUserPresenter = new UserPresenter(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.saveButton:
                mUserPresenter.saveUser(getID(), getFristName(),
                        getLastName());
                break;
            case R.id.loadButton:
                mUserPresenter.loadUser(getID());
                break;
            default:
                break;
        }
    }
}

  可以看到,View只负责处理与用户进行交互,并把数据相关的逻辑操作都扔给了Presenter去做。而Presenter调用Model处理完数据之后,再通过IUserView更新View显示信息。

2.1.1.4 总结:

  这个示例中创建了Model、View的接口而没有给Presenter创建接口。三者的联系是:1、在View的实现类UserActivity中创建UserPresenter类型的成员变量;2、在UserPresenter类中有Model和View的接口的引用,并在构造方法中参数传递View的接口实现类的实例并创建了Model接口实现类的实例;3、Model处理好数据后直接返回给Presenter。

2.1.2 方式二

  天气查询的 MVP 小 Demo:输入城市的代号,点击按钮获取城市的天气信息然后显示出来。
  原文:Android中的MVP

2.1.2.1 页面:

图片.png

2.1.2.2 目录结构:

目录结构.png

2.1.2.3 代码:

  • View的接口:
public interface WeatherView {
    void showLoading();
    void hideLoading();
    void showError();
    void setWeatherInfo(Weather weather);
}
  • View的实现类:
public class WeatherActivity extends BaseActivity implements WeatherView, View.OnClickListener {
    private WeatherPresenter weatherPresenter;

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

        weatherPresenter = new WeatherPresenterImpl(this); //传入WeatherView
    }
}
  • Presenter的接口:
public interface WeatherPresenter {
    /**
     * 获取天气的逻辑
     */
    void getWeather(String cityNO);
}
  • Presenter的实现类:
public class WeatherPresenterImpl implements WeatherPresenter, OnWeatherListener {
    /*Presenter作为中间层,持有View和Model的引用*/
    private WeatherView weatherView;
    private WeatherModel weatherModel;

    public WeatherPresenterImpl(WeatherView weatherView) {
        this.weatherView = weatherView;
        weatherModel = new WeatherModelImpl();
    }

    @Override
    public void getWeather(String cityNO) {
        weatherView.showLoading();
        weatherModel.loadWeather(cityNO, this);
    }

    @Override
    public void onSuccess(Weather weather) {
        weatherView.hideLoading();
        weatherView.setWeatherInfo(weather);
    }

    @Override
    public void onError() {
        weatherView.hideLoading();
        weatherView.showError();
    }
}
  • Model的接口。WeatherModel:
public interface WeatherModel {
    void loadWeather(String cityNO, OnWeatherListener listener);
}
  • OnWeatherListener回调接口。项目中在prestener文件夹下还有个OnWeatherListener文件,其在Presenter层实现,给Model层回调,更改View层的状态,确保Model层不直接操作View层。如果没有这一接口在WeatherPresenterImpl实现的话,WeatherPresenterImpl只有View和Model的引用那么Model怎么把结果告诉View呢?当然这只是一种解决方案,在实际项目中可以使用Dagger、EventBus、Otto等第三方框架结合进来达到更加松耦合的设计。
public interface OnWeatherListener {
    /**
     * 成功时回调
     * @param weather
     */
    void onSuccess(Weather weather);
    /**
     * 失败时回调,简单处理,没做什么
     */
    void onError();
}

2.1.2.4 总结:

  这个示例中创建了Model、View、Presenter的接口。三者的联系是:1、在View的实现类WeatherActivity中创建Presenter接口的引用然后用实现类WeatherPresenterImpl创建实例;2、在WeatherPresenterImpl中有Model和View的接口的引用,并在构造方法中参数传递View的接口实现类的实例并创建了Model接口实现类的实例;3、Model处理好数据后通过回调通知Presenter。

2.1.3 方式三

  下载文件小 Demo:点击按钮,下载一张图片,显示下载进度;下载完成后,在ImageView中显示这张图片。
  原文:Android MVP 十分钟入门

2.1.3.1 页面:

页面.png

2.1.3.2 目录结构:

目录结构.png

2.1.3.3 代码:

  • Model 接口定义所有需要实现的业务逻辑,在我们的下载任务中,业务逻辑只有一个,就是下载。因此Model 接口可以这么定义 :
public interface IDownloadModel {
    /**
     * 下载操作
     * @param url
     */
    void download(String url);
}
  • Model 具体实现:
public class DownloadModel implements IDownloadModel {
    private IDowndownPresenter mIDowndownPresenter;
    private MyHandler mMyHandler = new MyHandler();

    public DownloadModel(IDowndownPresenter IDowndownPresenter) {
        mIDowndownPresenter = IDowndownPresenter;
    }

    @Override
    public void download(String url) {
        HttpUtil.HttpGet(url, new DownloadCallback(mMyHandler));
    }

    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 300:
                    int percent = msg.arg1;
                    if (percent < 100) {
                        mIDowndownPresenter.downloadProgress(percent);
                    } else {
                        mIDowndownPresenter.downloadSuccess(Constants.LOCAL_FILE_PATH);
                    }
                    break;
                case 404:
                    mIDowndownPresenter.downloadFail();
                    break;
                default:
                    break;
            }
        }
    }
}
  • Presenter 接口:
public interface IDowndownPresenter {
    /**
     * 下载
     * @param url
     */
    void download(String url);

    /**
     * 下载成功
     * @param result
     */
    void downloadSuccess(String result);

    /**
     * 当前下载进度
     * @param progress
     */
    void downloadProgress(int progress);

    /**
     * 下载失败
     */
    void downloadFail();
}
  • Presenter 具体实现:
public class DownloadPresenter implements IDowndownPresenter {
    private IDownloadView mIDownloadView;
    private IDownloadModel mIDownloadModel;

    public DownloadPresenter(IDownloadView IDownloadView) {
        mIDownloadView = IDownloadView;
        mIDownloadModel = new DownloadModel(this);
    }

    @Override
    public void download(String url) {
        mIDownloadView.showProgressBar(true);
        mIDownloadModel.download(url);
    }

    @Override
    public void downloadSuccess(String result) {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.setView(result);
    }

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }

    @Override
    public void downloadFail() {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.showFailToast();
    }
}
  • View接口:
public interface IDownloadView {
    /**
     * 显示进度条
     * @param show
     */
    void showProgressBar(boolean show);

    /**
     * 设置进度条进度
     * @param progress
     */
    void setProcessProgress(int progress);

    /**
     * 根据数据设置view
     * @param result
     */
    void setView(String result);

    /**
     * 设置请求失败时的view
     */
    void showFailToast();
}
  • View具体实现:
public class MVPActivity extends AppCompatActivity implements IDownloadView {
    private DownloadPresenter mDownloadPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp);
        
        mDownloadPresenter = new DownloadPresenter(this);
    }
}

2.1.3.4 总结:

  这个示例中创建了Model、View、Presenter的接口。三者的联系是:1、在View的实现类MVPActivity 中创建Presenter的实现类DownloadPresenter类型的成员变量;2、在DownloadPresenter中有Model和View的接口的引用,并在构造方法中参数传递View接口实现类的实例并创建了Model接口实现类的实例;3、这个示例中Model持有Presenter的引用,处理完数据后直接调用Presenter的方法通知Presenter。

2.1.4 方式四

  Google官方MVP示例:实现了一个类似记事本的功能。
  项目地址:Google官方MVP示例

2.1.4.1 页面:

页面.png

2.1.4.2 目录结构(按功能模块分Package):

目录结构.png

2.1.4.3 代码:

项目中有两个基类:

  • BaseView:
public interface BaseView<T> { 
    void setPresenter(T presenter); 
}
  • BasePresenter:
public interface BasePresenter {
    void start();
}

  各自简单的声明了一个方法,start()方法用来给Presenter做一些初始化的操作不用特别在意,BaseView声明的方法就很有意思了,setPresenter(T presenter) 很明显是给View绑定Presenter,而上面介绍的几个项目使用的方式都是给Presenter传入View的引用来完成View和Presenter绑定的操作,这里谷歌采用了相反的方式来操作,具体原因后面会解释。
  单个模块的具体文件结构:


文件结构.png

  有Activity,Fragment,Presenter。View哪里去了? 其实谷歌的MVP是将Fragment作为View层来实现的,这一点在官方的Readme中也有说明,为什么要用Fragment?

  1、官方认为Fragment和Activity相比更像是MVP中的View层,刚好可以满足MVP的View层的要求,Activity则作为最高指挥官用来创建和联系View和Presenter。
  2、Fragment在平板或者屏幕上有多个View时更有优势。

  Activity只作为创建和联系View和Presenter的存在,而Fragment作为显示UI的存在。Activity主指挥,Fragment主显示。这也是谷歌sample中的一贯做法。
  View的问题解释完了,再看看TaskDetailContract 这种**Contract 接口,这也是官方独有的管理方法。
  Google的 todomvp 工程中每个模块都会有一个 **Contract 接口,来看看他的代码:

public interface TaskDetailContract {
    interface View extends BaseView<Presenter> {
        void setLoadingIndicator(boolean active);
        void showMissingTask();
        void hideTitle();
        void showTitle(String title);
        void hideDescription();
        void showDescription(String description);
        void showCompletionStatus(boolean complete);
        void showEditTask(String taskId);
        void showTaskDeleted();
        void showTaskMarkedComplete();
        void showTaskMarkedActive();
        boolean isActive();
    }
    interface Presenter extends BasePresenter {
        void editTask();
        void deleteTask();
        void completeTask();
        void activateTask();
    }
}

  Contract其实就是一个包涵了Presenter和View的接口。Presenter实现的逻辑层方法,View实现的UI层的方法都能在Contract接口中一目了然,具体的Presenter和View的实现类都是通过实现Contract接口来完成。这种方式既方便了管理和维护,也给开发点了一个导航灯。
  下面来看看Presenter如何引用到Fragment中以及View如何与Presenter建立联系。
  TaskDetailActivity :

public class TaskDetailActivity extends AppCompatActivity {
    public static final String EXTRA_TASK_ID = "TASK_ID";

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

        // Get the requested task id
        String taskId = getIntent().getStringExtra(EXTRA_TASK_ID);

        // 实例化taskDetailFragment
        TaskDetailFragment taskDetailFragment = (TaskDetailFragment)getSupportFragmentManager()
                .findFragmentById(R.id.contentFrame);
        if (taskDetailFragment == null) {
            //taskDetailFragment 添加到Activity
            taskDetailFragment = TaskDetailFragment.newInstance(taskId);
            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    taskDetailFragment, R.id.contentFrame);
        }

        // Create the presenter  初始化Presenter
        new TaskDetailPresenter(taskId, Injection.provideTasksRepository(getApplicationContext()),
                taskDetailFragment);
    }
}

  TaskDetailActivity主要做了taskDetailFragment和TaskDetailPresenter的初始化操作。在做TaskDetailPresenter初始化时直接将taskDetailFragment作为参数传入,因为taskDetailFragment实现了View层的接口,看到这里有一个疑问,new TaskDetailPresenter()可以实例化一个Presenter,但是怎么传入到taskDetailFragment中呢?为了解开疑惑我们查看一下TaskDetailPresenterde 的构造方法:

public class TaskDetailPresenter implements TaskDetailContract.Presenter {
    public TaskDetailPresenter(@Nullable String taskId,
                               @NonNull TasksRepository tasksRepository,
                               @NonNull TaskDetailContract.View taskDetailView) {
        this.mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");
        mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!");
        //实现绑定Presenter的关键方法
        mTaskDetailView.setPresenter(this);
    }
}

  关键方法就是在BaseView中声明的void setPresenter(T presenter)方法实现的绑定,当然这里只是调用View的方法,具体的绑定还要追踪到实体类中,来看TaskDetailFragment:

public class TaskDetailFragment extends Fragment implements TaskDetailContract.View {
    //声明了一个mPresenter
    private TaskDetailContract.Presenter mPresenter;

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.taskdetail_frag, container, false);
        return root;
    }

    @Override
    public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) {
        //完成mPresenter的绑定
        mPresenter = checkNotNull(presenter);
    }
}

  TaskDetailFragment 实现了TaskDetailContract接口中的View接口,这样在TaskDetailPresenter 构造方法中调用mTaskDetailView.setPresenter(this)方法后,实际调用的是TaskDetailFragment 中的setPresenter方法,TaskDetailPresenter 就成功绑定到了TaskDetailFragment 中。
  Model层
  该项目中Model层最大的特点是被赋予了数据获取的职责,与我们平常Model层只定义实体对象截然不同。实例中,数据的获取、存储、数据状态变化都是Model层的任务,Presenter会根据需要调用该层的数据处理逻辑并在需要时将回调传入。
  我们来看TaskDetailPresenter 的 openTask() 方法:

private void openTask() {
    // 判空处理
    if (null == mTaskId || mTaskId.isEmpty()) {
        mTaskDetailView.showMissingTask();
        return;
    }
    // 更新状态
    mTaskDetailView.setLoadingIndicator(true);
    // 获取该条Task数据
    mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() {
        @Override
        public void onTaskLoaded(Task task) {
            // The view may not be able to handle UI updates anymore
            // View已经被用户回退
            if (!mTaskDetailView.isActive()) {
                return;
            }
            // 获取到task数据,并更新UI
            mTaskDetailView.setLoadingIndicator(false);
            if (null == task) {
                mTaskDetailView.showMissingTask();
            } else {
                showTask(task);
            }
        }

        @Override
        public void onDataNotAvailable() {
            // The view may not be able to handle UI updates anymore
            // 显示数据获取失败时的状态
            if (!mTaskDetailView.isActive()) {
                return;
            }
            mTaskDetailView.showMissingTask();
        }
    });
}

  我们接着看 TasksRepository 中的getTask() 方法:

@Override
public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
    // 判空处理
    checkNotNull(taskId);
    checkNotNull(callback);

    // 获取缓存数据
    Task cachedTask = getTaskWithId(taskId);
    // Respond immediately with cache if available
    if (cachedTask != null) {
        callback.onTaskLoaded(cachedTask);
        return;
    }

    // Load from server/persisted if needed.
    // Is the task in the local data source? If not, query the network.
    // 从本地数据源(SQLite数据库)中获取
    mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
        @Override
        public void onTaskLoaded(Task task) {
            // 成功,则回调
            callback.onTaskLoaded(task);
        }

        @Override
        public void onDataNotAvailable() {
            // 失败,则从远程数据源(网络)中获取
            mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                @Override
                public void onTaskLoaded(Task task) {
                    // 回调成功时的方法
                    callback.onTaskLoaded(task);
                }

                @Override
                public void onDataNotAvailable() {
                    // 回调失败时的方法
                    callback.onDataNotAvailable();
                }
            });
        }
    });
}

  我们发现 TasksRepository 维护了两个数据源,一个是本地(SQLite数据库),一个是远程(网络服务器):

private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;

  我们发现他们(包括TasksRepository类)都实现了 TasksDataSource 接口:

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

  这样一来我们就很容易扩展新的数据源(获取数据的方式),毕竟我们在TaskDetailActivity中初始化TasksRepository就是调用的如下方法,其实我们很容易把FakeTasksRemoteDataSource替换为TasksRemoteDataSource,把TasksLocalDataSource 替换为TasksContentProviderDataSource,这就是针对接口编程的好处吧。

public static TasksRepository provideTasksRepository(@NonNull Context context) {
    checkNotNull(context);
    ToDoDatabase database = ToDoDatabase.getInstance(context);
    return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
            TasksLocalDataSource.getInstance(new AppExecutors(),
                    database.taskDao()));
}

2.1.4.4 总结:

  这个示例中创建了View、Presenter的接口,两者被包含在Contract中具体功能一目了然,统一管理方便维护。Model层的接口是TasksDataSource,而且该示例中的Model层使用了单例模式,在Model具体的实现类中实现对数据的不同操作(增、删、改、查),所以项目中不同的功能使用的是同一个Model对象,这和其他示例有所区别(其他项目一般是一个功能对应一个Model)。三者的联系是:1、该示例中Activity只作为创建和联系View和Presenter而存在,将Fragment作为显示UI的存在。Activity主指挥,Fragment主显示;2、在TaskDetailPresenter 中有Model和View的引用,并在构造方法中参数传递View、Model接口实现类的实例并调用View的setPresenter方法把自身的引用传给View;3、这个示例的Activity在创建Presenter时获取Model的单例并传给Presenter ,Model处理好数据后通过回调通知Presenter。

2.1.4.5 UML结构图:

UML结构图.png

2.1.5 汇总

  MVP更多的是一种思想,而不是一种固定的模式,所以不同的人有不同的实现方式。

  • 接口方面:都会给Model、View创建接口,有些项目不会创建Presenter的接口,极个别的项目也会不给Model创建接口,Google官方示例中引入Contract统一管理View、Presenter接口。
  • 三者的联系:1、在View的实现类Activity中包含Presenter成员变量(数据类型有些是接口有些是实现类);2、在Presenter中有Model和View的接口的引用,并在构造方法中参数传递View接口实现类的实例并创建了Model接口实现类的实例;3、Model处理完数据后如果逻辑简单可以直接返回数据给Presenter,如果逻辑复杂或是耗时操作可以通过回调或者持有Presenter引用的方式通知Presenter,也可以使用Dagger、EventBus、Otto等第三方框架达到更加松耦合的设计。注:Google官方示例有所区别,需单独看待。

2.1.6 接口的必要性

  可能有的人会问,为什么要写一个 View 的接口,直接把 Activity 本身传入到 Presenter 不行吗?这当然是可行的,这里使用接口主要是为了代码的复用,试想一下,如果直接传入 Activity,那么这个 Presenter 就只能为这一个 Activity 服务。举个例子,假设有个 App 已经开发完成了,可以在手机上正常使用,现在要求做平板上的适配,在平板上的界面显示效果有所变化,TextView 并不是直接在 Activity 中的,而是在 Fragment 里面,如果没有使用 View 的接口的话,那就需要再写一个针对 Fragment 的 Presenter,然后把整个过程再来一遍。但是使用 View 的接口就很简单了,直接让 Fragment 实现这个接口,然后复写接口里面的方法,Presenter 和 Model 层都不需要做任何改动。同理,Model 层也可以采用接口的方式来写。

2.1.7 目录结构

  项目的目录结构也有不同的实现。有些是按模块分Package,在模块下面再去创建Model、View、Presenter的子Package,如图:


按模块分.png

  当然也可以用Model、View、Presenter作为顶级的Package,然后把所有的模块的Model、View、Presenter类都到这三个顶级Package中,如图:


M、V、P作为顶级目录.png

2.2 进阶

2.2.1 内存泄漏

  写MVP的时候,Presenter会持有View,如果Presenter有后台异步的长时间的动作,比如网络请求,这时如果返回退出了Activity,后台异步的动作不会立即停止,这里就会有内存泄漏的隐患。
  解决方法:

  1. 在Presenter传入View实例引用时,通过 弱引用 进行封装。
  2. 在Presenter中提供 绑定(attach)与 解绑(detach)函数,以便调用者可以管理内存释放。

  代码如下:

/**
 * Presenter.java
 * 将传入的View接口实例,通过弱引用(WeakReference)把Presenter与View进行绑定。
 * @param view  界面更新接口实例
 */
public void attach(View view){
    aviewWeakReference=new WeakReference<>(view);
}
/**
 *  Presenter.java
 *  将Presenter与View进行解绑,并释放内存
 */
public void detach(){
    if(aviewWeakReference!=null){
        aviewWeakReference.clear();
        aviewWeakReference=null;
    }
}
/**
 *  Presenter.java
 *  或者简单的将View置空
 */
public void detach(){
    view = null;
}
/*************************分割线********************************/
/**
 * View.java
 * 退出时销毁持有View
 */
@Override
protected void onDestroy() {
    presenter.detachView();
    super.onDestroy();
}

  虽然这里 Activity 不会内存泄漏了,但是当 Activity 退出之后,Model 中请求数据就没有意义了,所以还应该在 detach 方法中,把 Model中的获取数据的任务取消,避免造成资源浪费。如果没有取消,后台的延时操作返回时,这个时候view被销毁了,如果接着去调用View的方法就会抛出空指针异常。所以在后台的延时操作中需要考虑到这种可能产生空指针的情况,尤其是网络请求。所以Google官方的实例中也有对这种情况的处理,如下所示:
  在View的接口中定义了一个isActive方法:

public interface TaskDetailContract {
    interface View extends BaseView<Presenter> {
        boolean isActive();
    }
}

  具体实现类:

public class TaskDetailFragment extends Fragment implements TaskDetailContract.View {
    @Override
    public boolean isActive() {
        return isAdded();
    }
}

  isAdded()方法如果返回true代表Fragment添加到了Activity,false代表没有添加,具体使用:

mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() {
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        // View已经被用户回退
        if (!mTaskDetailView.isActive()) {
            return;
        }
        // 获取到task数据,并更新UI
        mTaskDetailView.setLoadingIndicator(false);
        if (null == task) {
            mTaskDetailView.showMissingTask();
        } else {
            showTask(task);
        }
    }

    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        // 显示数据获取失败时的状态
        if (!mTaskDetailView.isActive()) {
            return;
        }
        mTaskDetailView.showMissingTask();
    }
});

2.2.2 MVP 的封装

  很显然,MVP 的实现套路是大致相同的,如果在一个应用中,存在大量的 Activity 和 Fragment,并且都使用 MVP 的架构,那么难免会有很多重复工作,所以封装就很有必要性了。
  首先 Model、View 和 Presenter 都可能会有一些通用性的操作,所以可以分别定义三个对应的底层接口。

interface BaseModel {
}

interface BaseView {
    void showError(String msg);
}

public abstract class BasePresenter<V extends BaseView, M extends BaseModel> {
    private WeakReference weakReference;
    protected M model;

    public BasePresenter() {
        model = createModel();
    }

    /**
     * 将传入的View接口实例,通过弱引用(WeakReference)把Presenter与View进行绑定。
     * @param v  界面更新接口实例
     */
    public void attach(V v){
        weakReference = new WeakReference<>(v);
    }

    /**
     *  将Presenter与View进行解绑,并释放内存
     */
    public void detach(){
        if(weakReference!=null){
            weakReference.clear();
            weakReference = null;
        }
    }

    abstract M createModel();
}

  这里的 View 层添加了一个通用的方法,显示错误信息,写在接口层,可以在实现处按照需求来显示,比如有的地方可能会是弹出一个 Toast,或者有的地方需要将错误信息显示在 TextView 中,Model 层也可以根据需要添加通用的方法,重点来看一下 Presenter 层。
  这里的 BasePresenter 采用了泛型,为什么要这么做呢?主要是因为 Presenter 必须同时持有 View 和 Model 的引用,但是在底层接口中无法确定他们的类型,只能确定他们是 BaseView 和 BaseModel 的子类,所以采用泛型的方式来引用,就巧妙的解决了这个问题,在 BasePresenter 的子类中只要定义好 View 和 Model 的类型,就会自动引用他们的对象了。Presenter 中的通用的方法主要就是 attachView 和 detachView,分别用于创建 View 对象的弱引用和把弱引用的对象置为空。前面已经说过,这样是为了防止内存泄漏。Model 的对象可以在 Presenter 的构造方法中创建。另外,这里的 Presenter 也可以写成接口的形式,读者可以按照自己的喜好来选择。
  然后看一下在业务代码中该如何使用 MVP 的封装,代码如下:

interface TestContract {
    interface Model extends BaseModel {
        void getData1(Callback1 callback1);
        void getData2(Callback2 callback2);
        void getData3(Callback3 callback3);
    }

    interface View extends BaseView {
        void updateUI1();
        void updateUI2();
        void updateUI3();
    }

    abstract class Presenter extends BasePresenter<View, Model> {
        abstract void request1();
        abstract void request2();
        void request3() {
            model.getData3(new Callback3() {
                @Override
                public void onResult(String text) {
                    view.updateUI3();
                }
            });
        }
    }
}

  首先定义一个 Contract 契约接口,然后把 Model、View、和 Presenter 的子类分别放入 Contract 的内部,这里的一个 Contract 就对应一个页面(一个 Activity 或者一个 Fragment),放在 Contract 内部是为了让同一个页面的逻辑方法都放在一起,方便查看和修改。Presenter 中的 request3 方法演示了如何通过 Presenter 来进行 View 和 Model 的交互。
  接下来要做的就是实现这三个模块的逻辑方法了,在 Activity 或 Fragment 中实现 TextContract.View 的接口,再分别创建两个类用来实现 TextContract.Model 和 TextContract.Presenter,复写里面的抽象方法就好了。
  每一个Activity都需要 初始化 Presenter与调用其绑定、解绑的方法,编写一个BaseActivity类,向子类提供Presenter初始化的抽象函数,并在BaseActivity中onCreate()与onDestory中调用对应Presenter类的attach()与detach()方法。(Fragment同理)

/**
 * BaseActivty:封装Presenter的绑定与解绑方法,减少相同冗余代码
 * @param <V> View界面
 * @param <P> Presenter
 */
public abstract class BaseAcitvity<V, P extends BasePresenter<V>> extends AppCompatActivity {
    protected P presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = createPresenter();
        if(presenter!=null){
            presenter.attach((V) this);
        }
    }

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

    /**
     * 创建继承于BasePresenter的子类
     * @return
     */
    protected abstract P createPresenter();
}

2.2.3 引入RxJava

  Model层中的方法一般都会传入一个回调接口,这是因为获取数据往往是异步的,在获取的数据时需要用回调接口通知Presenter来更新View。
  如果想要避免回调接口,可以采用RxJava的方式来Model获取的数据直接返回一个 Observable,接下来用RxJava的方式来改造的例子:

public class HttpModel {
    public Observable<String> request() {
        return Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                Thread.sleep(2000);
                emitter.onNext("从网络获取到的数据");
                emitter.onComplete();
            }
        }).subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());
    }
}

public class Presenter {
    private MVPView view;
    private HttpModel model;

    public Presenter(MVPView view) {
        this.view = view;
        model = new HttpModel();
    }

    private Disposable disposable;

    public void request() {
        disposable = model.request()
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String s) throws Exception {
                        view.updateTv(s);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {

                    }
                });
    }

    public void detachView() {
        view = null;
        if (disposable != null && !disposable.isDisposed()) {
            disposable.dispose();
        }
    }
}

  Model的 request方法直接返回一个Observable,然后在Presenter中调用subscribe方法来通知View更新,这样就避免了使用回调接口。

3 优缺点

3.1 优点

  1. 降低耦合度,实现了Model和View真正的完全分离,可以修改View而不影响Model。
  2. 模块职责划分明显,层次清晰。
  3. 隐藏数据。
  4. Presenter可以复用,一个Presenter可以用于多个View,而不需要更改Presenter的逻辑(当然是在View的改动不影响业务逻辑的前提下)。
  5. 利于测试驱动开发。以前的Android开发是难以进行单元测试的(虽然很多Android开发者都没有写过测试用例,但是随着项目变得越来越复杂,没有测试是很难保证软件质量的;而且近几年来Android上的测试框架已经有了长足的发展——开始写测试用例吧),在使用MVP的项目中Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。
  6. View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。
  7. 代码灵活性。

3.2 缺点

  1. Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。
  2. 由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。
  3. 如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。
  4. 额外的代码复杂度及学习成本。

4 参考文档

  1. Android MVP 详解(上)
  2. Android MVP 详解(下)
  3. Android MVP 模式
  4. Android MVP 十分钟入门
  5. ANDROID MVP 模式 简单易懂的介绍方式
  6. Android MVP 架构的自述
  7. MVP模式从入门到精通
  8. 浅谈 MVP in Android
  9. 一篇文章彻底搞懂 MVP
  10. 如何更高效的使用MVP以及官方MVP架构解析
  11. Android架构:2018 主流大厂MVP模式是怎样
  12. Google 官网MVP解读
  13. Android官方MVP架构项目解析

5 源码

源码地址:GsMVP

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容