东莞市盛裕绒艺玩具有限公司

东莞市盛裕绒艺玩具有限公司

大班taipanbet官方入口

15183418431
联系方式
全国服务热线: 15183418431

咨询热线:15532104509
联系人:刘桂忠
地址:海南省三亚市解放路1072号

MVP模式, 开源库mosby的使用及代码分析

来源:大班taipanbet官方入口   发布时间:2019-07-02   点击量:120

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 Github RepoMosby Getting StartedMosby MVP

使用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

相关产品

COPYRIGHTS©2017 大班taipanbet官方入口 ALL RIGHTS RESERVED 备案号:120