android:浅谈 mvp 架构

虽然谷歌继 mvp 之后,又推出了 mvvm 架构。不过,从官方示例仓库来看,谷歌是比较喜欢 mvp 架构的。因为看其介绍,后面的其他示例几乎都是基于 mvp 架构的代码。

官方示例代码仓库:todo-mvp

话说这个仓库出来很早了,从提交日志可以看到 在 2015-10 就开始了,然后也是在 2015 年底,就完成了该示例代码。不过一直都有维护,目前已经把 数据库 换成了 谷歌新推出的 JetPack 里面的 ROOM(不翻墙)来实现了。

由于我所在的项目组一直没有去使用这种架构模式,然后我自己也没有主动去学习过。所以,对于 mvp 我是比较陌生的。乘着这个周末,看了一下谷歌的示例代码,然后自己写了一个四不像的 todo-MVP。

在看谷歌示例代码todo-mvp 之前,我看到这个名字,以为是在 mvp 架构的基础上,又添加了新的思想 “TODO”。不过事实不是这样子的。这个项目,实现的功能就是做了一个 待办事项 清单,然后可以增删改查,设置清单已完成/未完成这样子。然后这些数据全部保存在了本地数据库里面。

其实这个功能跟我之前做过的 便签 app 极为相似;只不过这里不需要插入图片;但是这种架构风格我是极为陌生的。

我一直不太理解架构这个概念,似乎跟设计模式沾边,似乎跟代码风格也沾边;但是又不是这些东西。

在看了谷歌示例代码todo-mvp ,我对架构的粗浅理解就是,是对代码整体实现思路的一种描述,表示要以哪种方式去组织代码。

就比如要去实现这样一个待办事项 APP ,相信很多人都能完成。数据存储这块,就数据库去完成;然后界面展示就是 ListView/RecyclerView ,编辑添加界面就是 EditText 组合就好了。

这样一想,这个APP的整个功能就实现了。

不过,这还没细致到架构层面。架构层面,需要考虑,数据通过什么方式显示到界面,界面的操作,又是怎样改变持久化数据的。

以 mvp 为例:(具体例子就是:谷歌 sample todo-mvp

界面要展示数据,那么界面是什么,按照 mvp 里面的定义,界面是 View , 不过这个 View是一个概念,具体来说是一个接口;具体实现类可以是一个Activity/Fragment等;sample 里面的具体实现是一个Fragment implement View.

数据在哪里?示例代码中的数据是基于本地数据库的,那么数据源就是数据库;

界面要展示数据,界面,也就是 这个抽象 View 要怎么拿到 数据库中数据? 如果不采取任何架构,我们可以直接在 Fragment 里面去获取数据库相关对象,然后调用对象的查询方法去获取到数据;mvp 模式中不是这样做的。mvp 中,View 是不会获取任何数据库相关对象的,也不会去调用数据库相关的任何静态方法去获取数据。而是,这种获取数据的操作,让 Presenter 去做;Presenter 拿到数据之后,也不是再让 View 去调用 Presenter 然后获取数据进行展示,而是 Presenter 会持有一个 对应的 View ,然后 Presenter 在拿到数据的,调用 View 的实现好的方法去展示数据。

NOTE: 上述的 View 全部是 mvp 中的 v 的概念,具体来讲是一个接口;而不是 android.view.View对象.

从上面的介绍中可以看到,p 跟 v 都是接口,且他们是一一对应的,然后都要定义一些方法让实现类去实现。

比如看一下 这个 示例代码todo-mvp 里面的列表界面定义的 p 和 v 是什么样的。

/**
 *  谷歌示例代码 todo-mvp 列表界面定义的 p 和 v 
 * This specifies the contract between the view and the presenter.
 */
public interface TasksContract {

    interface View extends BaseView<Presenter> {

        void setLoadingIndicator(boolean active);

        void showTasks(List<Task> tasks);

        void showAddTask();

        void showTaskDetailsUi(String taskId);

        void showTaskMarkedComplete();

        void showTaskMarkedActive();

        void showCompletedTasksCleared();

        void showLoadingTasksError();

        void showNoTasks();

        void showActiveFilterLabel();

        void showCompletedFilterLabel();

        void showAllFilterLabel();

        void showNoActiveTasks();

        void showNoCompletedTasks();

        void showSuccessfullySavedMessage();

        boolean isActive();

        void showFilteringPopUpMenu();
    }

    interface Presenter extends BasePresenter {

        void result(int requestCode, int resultCode);

        void loadTasks(boolean forceUpdate);

        void addNewTask();

        void openTaskDetails(@NonNull Task requestedTask);

        void completeTask(@NonNull Task completedTask);

        void activateTask(@NonNull Task activeTask);

        void clearCompletedTasks();

        void setFiltering(TasksFilterType requestType);

        TasksFilterType getFiltering();
    }
}

可以看到 接口里面定义的方法还是蛮多的。从方法名大致可以看到 v 里面要实现:显示无任务的界面,显示加载中的界面,显示全部任务的界面等等; p 里面要实现加载任务;设置任务已完成,设置任务未完成等操作;

然后,在具体的实现中,v 的逻辑比较少,一般就是loadingView.setVisibility(GONE); contentView.setVisibility(Visiable);这些; p 的逻辑会比较多;会去异步获取数据,然后拿到数据后,调用对应的 v 里面的 显示数据/显示空/加载中的方法去进行界面的不同状态显示这些。

然而,这些操作目前都还没被触发,即使目前已经实现了 p 和 v 的实际子类;p 里面有个方法:void start(); 示例代码中,在该方法里面进行获取本地数据库数据的操作,在获取数据的回调中进行了显示不同界面的ui 切换逻辑。然后,这个 p.start();是什么时候被调用的呢?是在 Fragment.onResume() 里面调用的。

对应的, v 里面也有一个 关键方法void setPresenter(Presenter p); 。这个方法有什么用呢,或者说,为什么要让 v 也持有对应的 p 呢?

看示例代码了解到,v 的界面展示的确是被 p 所控制了。但是 v 的行为,p 控制不了,也就是用户的点击,触摸这些,p 是不能感知的。但是比如一个点击事件,里面又会产生不少具体逻辑,比如勾选未完成为已经完成了,那么,实际上是要改变数据的记录的;这时候,v 就可以调用 p 对应的方法去实现数据表的更新了。(这可能会触发连贯的逻辑:比如 p 里面完成了数据库的更新,然后 p 又会调用 v 里面定义的方法进行界面的刷新。)

通过以上分析,大致了解了 mvp 这种架构思想。

可以看出:

  • mvp 里面, m 是数据相关的,无论是本地数据库,还是网络请求获取的数据这些;增删改查这些都是 m 的逻辑;这个和一般的代码没有什么差别,任何风格的代码应该都会做对应的封装。
  • mvp 里面的 p 和 v 首先都是接口,都要定义相应的行为;通常来讲,应该是 首先定义 v 的行为:比如显示加载中,显示数据,显示空,去另一个界面 等等。然后 p 要进行对应的行为定义:比如加载数据,去另一个界面等;
  • 然后 p 和 v 是会互相持有的,也就是 p 里面会有 v 的对象,一般是成员对象;v 里面也会有 p 的对象,一般也是成员。(这一步看起来是必然的,但是也要说明清楚。)
  • 然后 p 里面的start()方法首先会被 v 调用,可以是 onCreate()/onStart()/onResume()这些方法里面去进行调用;对应的 ,p 里面的 start()方法会执行加载数据的逻辑,这种肯定是需要异步,然后在回调里面又去调用 v 对应的显示数据的方法去进行界面的展示。
  • 然后 v 的行为,用户点击这些需要执行的逻辑,又会去调用 p 去执行真正的逻辑。
  • p 的具体实现就是一个普通的类,但是要实现之前定义的接口里面的全部方法;v 的具体实现一般就是Fragment,也可以是 Activity. 不过看官方示例的介绍,似乎推荐用Fragment.官方示例的 mvp ,其实可以认为是 mvpc ,然后这里的 c 就是 Activity; c 里面去做 实例化 v 和 p 的操作。

衍生:比如一个 Activity 里面有多个 Fragment , 那么对应的,一个 Fragment 是一个 v, 要有一个对应的 p , c 还是那个 Activity; 这时候 c 就要去实例化全部的 p 和 v 。


然后,其实这个项目里面除了展示了 mvp 架构,还有一些额外的东西吸引到我。

  • 代码里面的异步回调,没有去显式的使用 Handler, 也没有去使用AsyncTask , 而是使用了一个自定义的类AppExecutors。这个类很有意思,也很巧妙。简单来说,就是实现了几组线程,分别对应 io 线程,main 线程,和 net 线程。
  • 对于 model 层,示例代码也给出了很深的封装。首先是,定义了一些概念:数据都叫 data source, 然后区分本地数据和远程数据,分别对应到local, remote; 数据库的增删改查这些本身是同步的(没有使用LiveData),那么对应的LocalDataScoure 就去做一个封装,全部实现成异步的,提供相应的回调出来。而且source不是为model服务的,是为 p 和 v 服务的,会根据界面的需要封装多个异步方法出来。(虽然示例代码里面没有 remote 相关数据,不过还是给出了对应的代码。
  • 对于 model 的封装还不止这个,因为数据有 local, remote的区分,所以,还得再次回归;定义一个TasksRepository , 这里会统一处理 local 和 remote 的数据,让 p 在调用数据的时候,直接调用 Respository 里面定义的异步方法就好了,不用管 local 和 remote。

从我个人的角度而言,我感觉封装有点过度了,无论是 model 层的封装,还是 p v 这种架构。p v 这种,我觉得不好的地方是,如果现在的界面是要展示一个列表,然后定义了对应的 p v 方法以及具体实现,然后,需求更新,这个界面只是做一个中转,会跳到一个新的界面去展示这些内容,那么 p v 接口以及具体实现全部要改;而,如果 model 按照示例代码中那样进行封装的话,那么 对应的 DataSource 以及 对应的 Repository 都要发生改动。改动量似乎有点大,不利于迭代。

求高手解惑,关于我对 mvp 的这些顾虑。

下面是部分示例代码展示:

// 使用 AppExecutors 进行线程切换
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final List<Task> tasks = mTasksDao.getTasks();
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (tasks.isEmpty()) {
                            // This will be called if the table is new or just empty.
                            callback.onDataNotAvailable();
                        } else {
                            callback.onTasksLoaded(tasks);
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
// AppExecutors, 替代 Handler , AsyncTask 进行线程切换
public class AppExecutors {

    private static final int THREAD_COUNT = 3;

    private final Executor diskIO;

    private final Executor networkIO;

    private final Executor mainThread;

    @VisibleForTesting
    AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) {
        this.diskIO = diskIO;
        this.networkIO = networkIO;
        this.mainThread = mainThread;
    }

    public AppExecutors() {
        this(new DiskIOThreadExecutor(), Executors.newFixedThreadPool(THREAD_COUNT),
                new MainThreadExecutor());
    }

    public Executor diskIO() {
        return diskIO;
    }

    public Executor networkIO() {
        return networkIO;
    }

    public Executor mainThread() {
        return mainThread;
    }

    private static class MainThreadExecutor implements Executor {
        private Handler mainThreadHandler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(@NonNull Runnable command) {
            mainThreadHandler.post(command);
        }
    }
}


// DiskIOThreadExecutor , 一个单线程的线程池...
public class DiskIOThreadExecutor implements Executor {

    private final Executor mDiskIO;

    public DiskIOThreadExecutor() {
        mDiskIO = Executors.newSingleThreadExecutor();
    }

    @Override
    public void execute(@NonNull Runnable command) {
        mDiskIO.execute(command);
    }
}

===========

update: 问过了一些经常使用 mvp 架构的开发,原来这个问题的确存在。

这个问题: 后期界面的行为发生变动,对应的 p v 接口及实现都需要做对应的修改。

而且,大佬们也没有很好的解决方案。大概也是得手动修改这些逻辑。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值