虽然谷歌继 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 接口及实现都需要做对应的修改。
而且,大佬们也没有很好的解决方案。大概也是得手动修改这些逻辑。