MVP模式, 开源库mosby的使用及代码分析
Android中的构架模式一直是一个很hot的topic, 近年来Architecture components推出之后, MVVM异军突起, 风头正在逐渐盖过之前的MVP.
其实我觉得MVP还是有好处的, 比如灵活多变(其实只是我用起来更熟悉顺手一些吧).
个人是没有什么偏见的, 关于项目的构架, 只要找到适合的就行.
最近打算实际用一下mosby这个开源库, 帮助构建一下mvp模式, 本文是我的心路历程和代码心得记录.
关于MVP模式
前几年MVP模式的风很大, 之前工作的项目也用的MVP模式, 所以对这个模式在team有很多讨论.
可以说一千个人眼中有一千种MVP吧, 比如Presenter之后的数据逻辑, 是用Interactor呢, 还是用Repository呢, 如果用了CursorLoader, 那么数据和View层直接耦合怎么办. 要不要给Presenter也定义接口呢, Presenter是注入呢还是在哪里(比如基类Fragment里)初始化呢, P和V的attach到底是在P里做呢还是在V里做呢.
MVP的原则
尽管结合项目实际, 可能有很多变种, 但是不管怎么变, MVP有几个原则是要遵守的:
- Activity/Fragment实现View接口, View中的方法都只是和UI显示相关的. View要尽可能的dummy, 不涉及业务逻辑, presenter告诉它干什么它干什么就行了.
- Presenter中没有Android相关的类, 是一个纯Java的程序. 这样有利于解耦和测试. (所以一个检查方法是看你的presenter的import中有没有android的包名.)
- 注意生命周期的处理, 因为异步任务callback返回之后View的状态不一定还是活跃的, 所以要有一定的措施检查View是否还在以及处理注销等, 避免crash或内存泄露.
MVP的官方例子
MVP模式Google有个官方例子: android-architecture, 我之前写了一篇解读在这里Google官方MVP Sample代码解读. (我刚看了一下官方sample代码又更新了, 还得再看一下.)
官方的例子属于比较正统的, 比如每个界面会定义一个Contract, 里面分别定义View和Presenter的接口. 用Repository包装local和remote的数据, local和remote的数据源会和repository实现相同的data source接口, 我非常喜欢RxJava版本的三级缓存处理.
我的一些小Demo
之前自己写的一些比较完整的使用MVP的Demo:
- TodoRealm: 一个Todo任务管理器, 只有本地数据.
- ZhihuDaily: 知乎日报, 支持离线模式.
MVVM
自从Google官方推出了Android Architecture Components之后, 看起来MVVM也是一种不错的选择.
这是官方的例子: android-architecture-components.
我还正在学习中, 关于这个话题可能以后会单独展开来讲一下, 我先沉淀一下.
目前的心得: 这一套东西也很强大, 就是用起来不太习惯. 要遵循的套路太多, 感觉没有使用MVP的时候那么自由. (可能还是不太熟的缘故吧, 我还是不多说了. ==!)
所以在学习这套模式的时候我突然又怀念起MVP模式, 准备把之前一个烂尾的个人项目重新拯救一把. 就是这个: GithubClient. 这一次准备用个mvp的库玩玩.
Mosby库的使用和代码分析
Mosby是一个帮你实现MVP或MVI的库.
最近看介绍才发现它的名字是根据How I met your mother这个美剧的主角起的. (我最近才利用生病期间看完这个剧. 觉得真是巧合啊, 注定要用一用了.)
之前都是自己手动实现MVP的, 也没什么难的, 用这个库会帮你解决什么问题呢?
看看Mosby的介绍:
使用Mosby的基本步骤:
- View接口继承
MvpView. - Presenter: 如果有规定Presenter接口, 接口继承
MvpPresenter<View>, 其中View是对应的View接口, 实现类继承MvpBasePresenter<View>.
如果没有Presenter的接口而直接是实现类也可以, 同样也是实现类继承MvpBasePresenter<View>. - Activity或Fragment实现View接口, 继承
MvpActivity或MvpFragment, 泛型参数类型传入对应的View接口和Presenter类型即可. - Activity或Fragment实现抽象的
createPresenter()方法, 在其中创建Presenter的实例.
好了, 所有必须的工作就做完了, mosby的类会处理初始化和实例保存等.
Activity/Fragment中不需要保存presenter的字段, Presenter中也不需要保存View的字段. 这些都在基类中保存了.
Mosby的实现
关于Mosby的实现可以查看它的类, 里面有详细的注释.
生命周期
MvpActivity中用了ActivityMvpDelegateImpl, 在Activity的每一个生命周期回调中做一些事情.
在onCreate()中创建了Presenter, 把它赋值给字段, 并且attachView(); 在onDestroy()中detachView()和调用presenter的destroy()来做一些清理工作.MvpFragment中用了FragmentMvpDelegateImpl, 在Fragment的生命周期中做一些事情: 在onCreate()中创建Presenter, 赋值给字段;onViewCreated()中attachView();onDestroyView()中detachView();onDestroy()中调用presenter的destroy()来做一些清理工作.
所以presenter的初始化, 和view的attach/detach, 以及它们变量的保存都是mosby帮我们处理好了.- mosby还支持ViewGroup作为View, 它提供了
MvpFrameLayout,MvpLinearLayout和MvpRelativeLayout以供继承, Delegate的实现类是ViewGroupMvpDelegateImpl, 用到的生命周期主要是onAttachedToWindow()和#onDetachedFromWindow().
Presenter中调用View的方法
MvpBasePresenter的实现没有什么特殊的, 主要是存了一个View的WeakReference. 新版中推荐使用ifViewAttached(ViewAction<V>)方法来把判断和执行一次性做了. 原来的isViewAttached()和getView()已经标记为deprecated了.
关于这样做的原因, 在这里有讨论: https://github.com/sockeqwe/mosby/issues/233.
屏幕旋转时的状态保存
mosby是处理了屏幕旋转时的状态保存的, 可以看到初始化ActivityMvpDelegateImpl时默认第三个参数是true, 即屏幕旋转时保存状态.
具体做法是通过PresenterManager把presenter保存起来.
保存的时候传了activity和一个生成的viewId:
private P createViewIdAndCreatePresenter() {
P presenter = delegateCallback.createPresenter();
if (presenter == null) {
throw new NullPointerException(
"Presenter returned from createPresenter() is null. Activity is " + activity);
}
if (keepPresenterInstance) {
mosbyViewId = UUID.randomUUID().toString();
PresenterManager.putPresenter(activity, mosbyViewId, presenter);
}
return presenter;
}
恢复状态的时候需要把之前存的Presenter拿出来还是用activity的实例和viewId:
@Nullable public static <P> P getPresenter(@NonNull Activity activity, @NonNull String viewId) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
if (viewId == null) {
throw new NullPointerException("View id is null");
}
ActivityScopedCache scopedCache = getActivityScope(activity);
return scopedCache == null ? null : (P) scopedCache.getPresenter(viewId);
}
其中viewId是通过bundle保存和恢复出来的:
@Override public void onSaveInstanceState(Bundle outState) {
if (keepPresenterInstance && outState != null) {
outState.putString(KEY_MOSBY_VIEW_ID, mosbyViewId);
if (DEBUG) {
Log.d(DEBUG_TAG,
"Saving MosbyViewId into Bundle. ViewId: " + mosbyViewId + " for view " + getMvpView());
}
}
}
那么问题来了:
- 1.既然我们已经有了一个viewId作为key, 为什么还需要activity来作为查询条件?
- 2.如果真的需要这个条件, 那么屏幕旋转以后activity都重建了, 如何通过新的activity实例获得之前的Presenter呢?
首先我是在代码中找到了第二个问题的答案, 即两个不同的activity是如何关联起来的:
static final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (savedInstanceState != null) {
String activityId = savedInstanceState.getString(KEY_ACTIVITY_ID);
if (activityId != null) {
// After a screen orientation change we map the newly created Activity to the same
// Activity ID as the previous activity has had (before screen orientation change)
activityIdMap.put(activity, activityId);
}
}
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
// Save the activityId into bundle so that the other
String activityId = activityIdMap.get(activity);
if (activityId != null) {
outState.putString(KEY_ACTIVITY_ID, activityId);
}
}
...
@Override public void onActivityDestroyed(Activity activity) {
if (!activity.isChangingConfigurations()) {
// Activity will be destroyed permanently, so reset the cache
String activityId = activityIdMap.get(activity);
if (activityId != null) {
ActivityScopedCache scopedCache = activityScopedCacheMap.get(activityId);
if (scopedCache != null) {
scopedCache.clear();
activityScopedCacheMap.remove(activityId);
}
// No Activity Scoped cache available, so unregister
if (activityScopedCacheMap.isEmpty()) {
// All Mosby related activities are destroyed, so we can remove the activity lifecylce listener
activity.getApplication()
.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
if (DEBUG) {
Log.d(DEBUG_TAG, "Unregistering ActivityLifecycleCallbacks");
}
}
}
}
activityIdMap.remove(activity);
}
};
通过Bundle存取传递一个activityId, 新创建的activity实例和旧的activity实例就有相同的id. 这个关系存储在Map<Activity, String> activityIdMap里.
这样在新的activity中通过map查询到activityId之后, 在Map<String, ActivityScopedCache> activityScopedCacheMap中再通过activityId查到了ActivityScopedCache对象, 再用viewId作为key查询到presenter.
看了onActivityDestroyed()部分的代码之后也终于明白了第一个问题的答案, 即这样做的原因, 如果只用viewId, 我们是解决了存放和查询, 但是没有解决释放的问题.
因为我们的需求只是在屏幕旋转的情况下保存presenter的实例, 我们仍然需要在activity真的销毁的时候释放对presenter实例的保存.
这里用了activity.isChangingConfigurations()的条件来区分activity是真的要销毁, 还是为了屏幕旋转要销毁.
PS: 说到状态保存和恢复, 之前的一篇博客写得很详细, 可以参考一下: Android Fragment使用(三) Activity, Fragment, WebView的状态保存和恢复
其他
Mosby还支持LCE(Loading-Content-Error)和ViewState, 为开发者省去更多套路化的代码, 还有处理屏幕旋转之后的状态恢复.
有空的时候再写一篇扒一扒吧.
欢迎关注微信公众号: 圣骑士Wind

MVP模式, 开源库mosby的使用及代码分析的更多相关文章
- iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects
Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...
- .Net平台-MVP模式初探(一)
为什么要写这篇文章 笔者当前正在负责研究所中一个项目,这个项目基于.NET平台,初步拟采用C/S部署体系,所以选择了Windows Forms作为其UI.经过几此迭代,我们发现了一个问题:虽然业务逻辑 ...
- Android架构篇--MVP模式的介绍篇
摘要: 在MVVM成熟之前MVP模式在Android上有被神化的趋势,笔者曾经在商业项目中从零开始大规模采用过MVP模式对项目进行开发.在使用MVP模式进行开发的时候发现项目的结构模式对开发是有一定的 ...
- MVP模式(Android)
以前在写项目的时候,没有过多考虑架构模式的问题,因为之前一直做J2EE开发,而J2EE都是采用MVC模式进行开发的,所以在搭建公司项目的时候,也是使用类似MVC的架构(严格来讲,之前的项目还算不上MV ...
- MVP模式在Android项目中的使用
以前在写项目的时候,没有过多考虑架构模式的问题,因为之前一直做J2EE开发,而J2EE都是采用MVC模式进行开发的,所以在搭建公司项目的时候,也是使用类似MVC的架构(严格来讲,之前的项目还算不上MV ...
- 100个Github上Android开源库
项目名称 项目简介 1. react-native 这个是 Facebook 在 React.js Conf 2015 大会上推出的基于 JavaScript 的开源框架 React Native, ...
- GitHub上排名前100的Android开源库介绍(来自github)
本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍,至于排名完全是根据 GitHub 搜索 Java 语言选择 (Best Match) 得到的结果,然后过滤了 ...
- GitHub Top 100的Android开源库
摘要: 本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍, 至于排名完全是根据GitHub搜索Java语言选择「Best M... 本项目主要对目前 GitH ...
- GitHub 上排名前 100 的 Android 开源库进行简单的介绍
若有任何疑问可通过邮件或微博联系我 项目名称 项目简介 1. react-native 这个是 Facebook 在 React.js Conf 2015 大会上推出的基于 JavaScript 的开 ...
随机推荐
- SSL/TLS握手过程
----------------------------------专栏导航----------------------------------HTTPS协议详解(一):HTTPS基础知识 HTTPS ...
- React中的通讯组件
1.父传子: 传递:当子组件在父组件中当做标签使用的时候,给当前子组件绑定一个自定义属性,值为需要传递的数据 接收:在子组件内部通过this.props进行接收 2.子传父 传 ...
- css3的动画特效--元素旋转(transition,animation)
开发中,视觉要你实现一个元素的旋转问题,比如说如下图所示: 思路:首先动画动效肯定离不开anmimation动画. 和transition动画一样,animation动画也是CSS3动画的一种,这类动 ...
- Solr 06 - Solr中配置使用IK分词器 (配置schema.xml)
目录 1 配置中文分词器 1.1 准备IK中文分词器 1.2 配置schema.xml文件 1.3 重启Tomcat并测试 2 配置业务域 2.1 准备商品数据 2.2 配置商品业务域 2.3 配置s ...
- InterlliJ Debug方式启动:method breakpoints may dramatically show down debugging
使用idea在DEBUG的时候出现Method breakpoints may dramatically slow down debugging, 如图: 根据语义可能是断点打在方法上面了,导致在某个 ...
- MySQL系列--1.安装卸载与用户权限管理
MySQL安装 1.Ubuntu18下安装MySQL sudo apt-get install mysql-server MySQL的版本为5.7.25 2.登录MySQL 采用mysql-serve ...
- Django的静态资源
如果你的静态资源是某个APP专属,那么就在这个APP目录下建立一个static目录,就像上图report这个APP中的static目录.当浏览这个APP的网页时它会从这里去找资源,当然,它首先会从共用 ...
- JavaScript面向对象--继承 (超简单易懂,小白专属)
一.继承的概念 子类共享父类的数据和方法的行为,就叫继承. 二.E55如何实现继承?探索JavaScript继承的本质 2.1构造函数之间的"复制粘贴" 第一条路是通过构造函数来继 ...
- 精读《React PowerPlug 源码》
1. 引言 React PowerPlug 是利用 render props 进行更好状态管理的工具库. React 项目中,一般一个文件就是一个类,状态最细粒度就是文件的粒度.然而文件粒度并非状态管 ...
- Spring Boot(九)Swagger2自动生成接口文档和Mock模拟数据
一.简介 在当下这个前后端分离的技术趋势下,前端工程师过度依赖后端工程师的接口和数据,给开发带来了两大问题: 问题一.后端接口查看难:要怎么调用?参数怎么传递?有几个参数?参数都代表什么含义? 问题二 ...