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接口, 继承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
微信公众号

posted @ 2018-09-25 08:40  圣骑士wind  阅读(2129)  评论(1编辑  收藏  举报