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接口, 继承MvpActivityMvpFragment, 泛型参数类型传入对应的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, MvpLinearLayoutMvpRelativeLayout以供继承, 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的使用及代码分析的更多相关文章

  1. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  2. .Net平台-MVP模式初探(一)

    为什么要写这篇文章 笔者当前正在负责研究所中一个项目,这个项目基于.NET平台,初步拟采用C/S部署体系,所以选择了Windows Forms作为其UI.经过几此迭代,我们发现了一个问题:虽然业务逻辑 ...

  3. Android架构篇--MVP模式的介绍篇

    摘要: 在MVVM成熟之前MVP模式在Android上有被神化的趋势,笔者曾经在商业项目中从零开始大规模采用过MVP模式对项目进行开发.在使用MVP模式进行开发的时候发现项目的结构模式对开发是有一定的 ...

  4. MVP模式(Android)

    以前在写项目的时候,没有过多考虑架构模式的问题,因为之前一直做J2EE开发,而J2EE都是采用MVC模式进行开发的,所以在搭建公司项目的时候,也是使用类似MVC的架构(严格来讲,之前的项目还算不上MV ...

  5. MVP模式在Android项目中的使用

    以前在写项目的时候,没有过多考虑架构模式的问题,因为之前一直做J2EE开发,而J2EE都是采用MVC模式进行开发的,所以在搭建公司项目的时候,也是使用类似MVC的架构(严格来讲,之前的项目还算不上MV ...

  6. 100个Github上Android开源库

    项目名称 项目简介 1. react-native 这个是 Facebook 在 React.js Conf 2015 大会上推出的基于 JavaScript 的开源框架 React Native, ...

  7. GitHub上排名前100的Android开源库介绍(来自github)

    本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍,至于排名完全是根据 GitHub 搜索 Java 语言选择 (Best Match) 得到的结果,然后过滤了 ...

  8. GitHub Top 100的Android开源库

    摘要: 本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍, 至于排名完全是根据GitHub搜索Java语言选择「Best M... 本项目主要对目前 GitH ...

  9. GitHub 上排名前 100 的 Android 开源库进行简单的介绍

    若有任何疑问可通过邮件或微博联系我 项目名称 项目简介 1. react-native 这个是 Facebook 在 React.js Conf 2015 大会上推出的基于 JavaScript 的开 ...

随机推荐

  1. C++ Opencv remap()重映射函数详解及使用示例

    一.重映射及remap()函数介绍 重映射,就是把一幅图像中某位置的像素放置到另一图像指定位置的过程.即: 在重映射过程中,图像的大小也可以同时发生改变.此时像素与像素之间的关系就不是一一对应关系,因 ...

  2. [Swift]LeetCode123. 买卖股票的最佳时机 III | Best Time to Buy and Sell Stock III

    Say you have an array for which the ith element is the price of a given stock on day i. Design an al ...

  3. [Swift]LeetCode891. 子序列宽度之和 | Sum of Subsequence Widths

    Given an array of integers A, consider all non-empty subsequences of A. For any sequence S, let the  ...

  4. mysql+postgresql备份与恢复

    mysql备份一个库, mysqldump  -u用户名 -p密码 [选项] [数据库名] > /备份路径/备份文件名 mysqldump -uuser -p123123 auth > / ...

  5. Kubernetes 笔记 07 豌豆荚之旅(二)

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. Hi,大家好, ...

  6. python的StringIO

    有时候需要将 information 保存在本地,可以这样写: file = open("filename","w") file.close() file.cl ...

  7. Java接口的实例应用:致敬我的偶像——何塞·穆里尼奥

    文/沉默王二 曹操在<短歌行>中为杜康酒打过一个价值一亿个亿的广告——“何以解忧,唯有杜康”,我替曹操感到惋惜的是他本人并不会收到这笔不菲的代言费.想一想,要是三国时期的明星人物们有这个代 ...

  8. peewee insert 数据时报错:'buffer' object has no attribute 'translate'

    错误信息: "'buffer' object has no attribute 'translate'" 场景:使用peewee insert 数据时,BlobField 字段存储 ...

  9. Chapter 4 Invitations——20

    Well, that was fine. I could leave him alone. 行吧,但愿一切都好.我能让他一个人. I would leave him alone. 我也会让他一个人的. ...

  10. HashMap,HashTable,ConcurrentHashMap异同比较

    0. 前言 HashMap和HashTable的区别一种比较简单的回答是: (1)HashMap是非线程安全的,HashTable是线程安全的. (2)HashMap的键和值都允许有null存在,而H ...