有关MVP&MVI的一些事

作者: LSteven | 来源:发表于2018-04-25 14:57 被阅读30次

    很老生常谈的架构,看了一下Mosby顺便总结了一下

    屏幕旋转

    MVP的架构太过流行懒得再写了。主要看怎么和屏幕旋转整合。一个框架优秀,就在于考虑完善。

    复习一下,一个Activity如果没有经过任何配置,在屏幕旋转后的生命周期为:
    onPause –> onSaveInstanceState –> onStop –> onDestroy –> onCreate –> onStart –> onRestoreInstanceState –> onResume

    We need to keep around the original state, in case we need to be created again. But we only do this for pre-Honeycomb apps, which always save their state when pausing, so we can not have them save their state when restarting from a paused state. For HC and later, we want to (and can) let the state be saved as the normal part of stopping the activity.

    static boolean retainPresenterInstance(boolean keepPresenterInstance, Activity activity) {
        return keepPresenterInstance && (activity.isChangingConfigurations()|| !activity.isFinishing());
    }
    
    @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());
          }
        }
    }
    

    activity.isChangingConfigurations()

    isChangingConfigurations用来检测当前的Activity是否因为Configuration的改变被销毁了,然后又使用新的Configuration来创建该Activity。所以我们看到在retainPresenterInstance中如果当前activitydestroy时正在改变配置(同时不finish)那就保存改Presenter

    每个Activity|Fragment|View会对应一个mosbyViewId,这个idpresenter相绑定,所以在onSaveInstanceState时需要保存这个id,在onCreate时恢复。

    至于PresenteronCreateattach,在onDestroydetach,如果是旋转屏幕则会保存Presenter,否则进行presenter.onDestroy

    onSaveInstanceState中,系统会通过mContentParent.saveHierarchyState(states)来保存整个ViewGroup的状态信息(View有idD且允许保存状态是可以保存的前提。)

    Fragment

    Fragment需要考虑的因素就比较多:

    • 屏幕旋转
    • 回退栈(Back Stack)

    屏幕旋转与activity相同,注意,如果fragment使用了setRetainInstance(true);那在旋转时不会进行onDestroy(不会Destroy代表不需要onCreate,即变量不会被销毁重建),只会onDetachView&onDetach。同时屏幕旋转会进行onSaved...

    如果fragment被加入回退栈,那当它被replace时它也不会onDestroy,只会onDestroyView,注意这里不会调用onSaved...

    static boolean retainPresenterInstance(Activity activity, Fragment fragment,
          boolean keepPresenterInstanceDuringScreenOrientationChanges,
          boolean keepPresenterOnBackstack) {
    
        if (activity.isChangingConfigurations()) {
          return keepPresenterInstanceDuringScreenOrientationChanges;
        }
    
        if (activity.isFinishing()) {
          return false;
        }
    
        if (keepPresenterOnBackstack && BackstackAccessor.isFragmentOnBackStack(fragment)) {
          return true;
        }
    
        return !fragment.isRemoving();
      }
    

    还是使用isChangingConfigurations来判断是否旋转,BackstackAccessor.isFragmentOnBackStack判断是否在回退栈中

    onViewCreated中进行attach动作,onDestroyViewdetach

    总结一下,分三种情况:

    • 旋转屏幕 setRetainInstance(false) 会onDestroy,所以presenter会变为空,需要在onSavexxx中设置mosbyId,onCreate中重新生成
    • 旋转屏幕 setRetainInstance(true) 同时不会走onDestroy&onCreate,presenter不会变空,所以没什么影响
    • 加入了回退栈 其实也不会走onDestroy,所以也没什么影响(说实话代码里个人觉得keepPresenterOnBackstack是多余的。。有同学知道可以留言一下)

    内存泄漏

    void attachView(@NonNull V view);
    void detachView();
    

    P层是会引用V层的,而V一般都是Activity 如果不及时释放会导致内存泄露。所以attach&detach是生命周期中必须调用的方法。

    MVI

    一开始没怎么看懂,还是举个例子

    1.png

    作者创建MVI是觉得不是所有时候model都是必须的,所以在MVI模式下主要有以下几个模式:

    • ViewState MVI把页面中的不同状态用VS表示,例如loading/success/fail,刚开始就把它看成model
    • Intent 一个意愿,与view有关,例如用户点击选中某个item/用户删除某个item,是一种行为模式(好哲学- -)
    • Presenter

    先简单概括一下整个流程,当用户点击某个按钮时发出intent,intent触发逻辑层的操作,最终进入一种ViewState即某种页面上可以显示的状态,然后进行渲染即可。所以在MVI中,页面渲染只有一种模式render(ViewState)。有什么好处? 前面花了很多时间在讲旋转啊等,那么在MVI模式中,对于旋转只需要记录旋转前的ViewState,然后旋转后进行render(VS)即可。

    Presenter是做啥的,它也只是起到连接解耦合的作用。

    image

    BehaviorSubject对象作为业务逻辑和View层的“中继”

    PublishSubject对象作为“中继”,View与Presenter

    Presenter初始化时创建viewStateBehaviorSubject = BehaviorSubject.create(); BehaviorSubject是个什么鬼呢:

     // observer will receive the "one", "two" and "three" events, but not "zero"
      BehaviorSubject<Object> subject = BehaviorSubject.create();
      subject.onNext("zero");
      subject.onNext("one");
      subject.subscribe(observer);
      subject.onNext("two");
      subject.onNext("three");
    

    对于BehaviorSubject来说,subscribe后会接受到前一个发射的item.

    如果是第一次attachView,那么会进行bindIntent

    attachView

    1.bindIntent

    bindIntent()方法是presenterintent和逻辑层绑定在一起。 很关键的函数,可以看个例子:

    @Override protected void bindIntents() {
        Observable<List<Product>> selectedItemsIntent =
            intent(ShoppingCartOverviewView::selectItemsIntent)
                .mergeWith(clearSelectionIntent.map(ignore -> Collections.emptyList()))
                .doOnNext(items -> Timber.d("intent: selected items %d", items.size()))
                .startWith(new ArrayList<Product>(0));
    
    
        subscribeViewState(selectedItemsIntent, ShoppingCartOverviewView::render);
      }
    
    

    intent:

      @MainThread protected <I> Observable<I> intent(ViewIntentBinder<V, I> binder) {
        PublishSubject<I> intentRelay = PublishSubject.create();
        intentRelaysBinders.add(new IntentRelayBinderPair<I>(intentRelay, binder));
        return intentRelay;
      }
    

    可以看到ShoppingCartOverviewView::selectItemsIntent是用户进行的操作,例如点击每个按钮等,用intent{}包裹后,它就和publicSubject绑定了,记住这句话,后面会用到。

    PublishSubject<Object> subject = PublishSubject.create();
    // observer1 will receive all onNext and onComplete events
    subject.subscribe(observer1);
    subject.onNext("one");
    subject.onNext("two");
    // observer2 will only receive "three" and onComplete
    subject.subscribe(observer2);
    subject.onNext("three");
    subject.onComplete();

    intentRelay与一个接口binder绑定在了一起。这里的binder就是前面的intent,例如view.loadData(一般就是一个Observable)

    subscribeViewState:

      @MainThread protected void subscribeViewState(@NonNull Observable<VS> viewStateObservable,
          @NonNull ViewStateConsumer<V, VS> consumer) {
        if (subscribeViewStateMethodCalled) {
          throw new IllegalStateException(
              "subscribeViewState() method is only allowed to be called once");
        }
        subscribeViewStateMethodCalled = true;
    
        if (viewStateObservable == null) {
          throw new NullPointerException("ViewState Observable is null");
        }
    
        if (consumer == null) {
          throw new NullPointerException("ViewStateBinder is null");
        }
    
        this.viewStateConsumer = consumer;
    
        viewStateDisposable = viewStateObservable.subscribeWith(
            new DisposableViewStateObserver<>(viewStateBehaviorSubject));
      }
    

    intent被触发时,即用户进行一个操作时,最终经过一系列形式转换成ViewState,光有这个ViewState也没有毛线用啊需要展示啊,所以需要通知一个consumer去进行render。 不过这里只是简单的设置viewStateConsumer = consumer

    2.绑定VS和消费者
    viewStateBehaviorSubject.subscribe(new Consumer<VS>() {
        @Override
        public void accept(VS vs) throws Exception {
            viewStateConsumer.accept(view, vs);
        }
    });
    

    当接收到新的VS时,viewStateBehavorSubject会通知consumer处理。这里consumer一般就是render方法。

    3.绑定意愿
    Observable<I> intent = intentBinder.bind(view);
    if (intent == null) {
        throw new NullPointerException(
                "Intent Observable returned from Binder " + intentBinder + " is null");
    }
    
    if (intentDisposables == null) {
        intentDisposables = new CompositeDisposable();
    }
    
    intentDisposables.add(intent.subscribeWith(new DisposableIntentObserver<I>(intentRelay)));
    

    绑定intentPublicSubject,这里intent就是用户的意愿,当用户发出意愿时会通知intentRelay,从而触发intent

    我们再回顾一遍整个流程,当用户点击发出意愿时,会通知PublishSubject,PublishSubject激活viewStateObservable,viewStateObservable经过一系列逻辑处理等到VS后通知viewStateBehaviorSubject,viewStateBehaviorSubject通知consumer进行渲染

    Reducer

    有一种场景考虑一下下拉刷新,我们想把拉回来的数据和已有的数据进行合并显示。这就要用到Reducer了,前端的同学肯定知道这是什么,oldState + 增量数据 = new State

    正好Rxjava给我们提供了scan()运算符,看个例子。

    Observable<PartialStateChanges> allIntentsObservable =
        Observable.merge(loadFirstPage, nextPage, pullToRefresh, loadMoreFromGroup)
            .observeOn(AndroidSchedulers.mainThread());
    
    HomeViewState initialState = new HomeViewState.Builder().firstPageLoading(true).build();
    
    subscribeViewState(
        allIntentsObservable.scan(initialState, this::viewStateReducer).distinctUntilChanged(),
        HomeView::render);
        
     
        
    private HomeViewState viewStateReducer(HomeViewState previousState,
      PartialStateChanges partialChanges) {
    }
    

    这里PartialStateChanges代表增量数据,利用scan得到最新的VS发射。

    ViewState

    onSaveInstanceState中进行viewState的保存。其实还是保存在bundle里,在onPostCreate里恢复viewState

    当屏幕旋转时,Mosby会将view detach from Presenter,

    @Override
    @CallSuper
    public void detachView() {
        detachView(true);
        if (viewRelayConsumerDisposable != null) {
            // Cancel subscription from View to viewState Relay
            viewRelayConsumerDisposable.dispose();
            viewRelayConsumerDisposable = null;
        }
    
        if (intentDisposables != null) {
            // Cancel subscriptions from view intents to intent Relays
            intentDisposables.dispose();
            intentDisposables = null;
        }
    }
    

    可以看到,在detachView时, viewRelayConsumerDisposableintentDisposabledispose,前者是VS->consumer连接点,后者是意愿(View)与PublishSubject的连接点

    而在destroy时:

    @Override
    @CallSuper
    public void destroy() {
        detachView(false);
        if (viewStateDisposable != null) {
            viewStateDisposable.dispose();
        }
        unbindIntents();
        reset();
    }
    

    viewStateObservableviewStateBehaviorSubject的断连。同时在unbindIntent由用户自定义presenter自行解除。

    总结一下:

    • 用户点击发出意愿时,会通知PublishSubject
    • PublishSubject激活viewStateObservable
    • viewStateObservable经过一系列逻辑处理等到VS后通知viewStateBehaviorSubject(BehaviorSubject)
    • viewStateBehaviorSubject通知consumer进行渲染

    所以detach时第一步和第四步断裂,destroy时第三步断裂。

    我们看BehaviorSubject的特性,即使view已经detach了,它仍然可以接收到来自逻辑层的更新通知,behaviorSubject在重新绑定(view reattach)时会发出最后一个值。所以发生变化后最后一次的VS仍然可以通知给consumer,

    clean architecture

    btw 随便插一句 很多人会提到clean architecture。

    image.png

    其实可以看成是mvpmvi的抽象吧。DataLayer就是M层,Domain是逻辑层,最后交给presenter去对view进行处理。

    这只是一种思想:

    • Independent of Frameworks.
    • Testable.
    • Independent of UI.
    • Independent of Database.
    • Independent of any external agency.

    issue

     Observable<RepositoryState> pullToRefreshData =
            intent(CountriesView::pullToRefreshIntent).switchMap(
                ignored -> repositroy.reload().switchMap(repoState -> {
                  if (repoState instanceof PullToRefreshError) {
                    // Let's show Snackbar for 2 seconds and then dismiss it
                    return Observable.timer(2, TimeUnit.SECONDS)
                        .map(ignoredTime -> new ShowCountries()) // Show just the list
                        .startWith(repoState); // repoState == PullToRefreshError
                  } else {
                    return Observable.just(repoState);
              }
        }));
    

    使用MVI会有一个神奇的问题。。前面说过BehaviorSubject在重新attach的时候会发出上一次的VS,那考虑一个场景。我拉取数据出错,会弹错误的提示,此时整个页面处于错误状态。这时候我新进入一个activity,然后返回上一个页面,因为重新attach..就又会弹一次错误提示。这明显就有问题。所以作者想了个解决方法:

     return Observable.timer(2, TimeUnit.SECONDS)
                        .map(ignoredTime -> new ShowCountries()) // Show just the list
                        .startWith(repoState); // repoState == PullToRefreshError
    

    经过2s延迟后把状态VS置为上一个状态,而不是一直保留错误的状态。

    然后上面issue提的是,如果你有个状态是打开Activity,那么同样返回后会不停打开Activity。。然后作者回复说他觉得打开Activity不应该作为页面的一种状态,而应该使用Navigator来做。

    class Navigator {
        private final Activity activity;
    
        @Inject
        public Navigator(Activity activity){
            this.activity = activity;
        }
    
        public void navigateToTestActivity(){
              TestClickActivity.start(activity);
        }
    }
    
     Observable<ProductDetailsViewState> clickTest =
            intent(ProductDetailsView::testBtnIntent)
            .map((aBoolean) ->  new ProductDetailsViewState.TestViewState())
            .doOnNext( aBoolean -> navigator.navigateToTestActivity() ); // Navigation as side effect
    

    把打开页面作为一种副作用。

    然后有个小哥很惨的表示他的P层完全作为一个模块独立开来的,他不能在P层去打开Activity..,所以他想了一种新的方法就是在onPause的时候把打开Activity这种状态回置。其实跟作者一开始提的思路差不多。

    参考资料

    Ted Mosby - 软件架构

    相关文章

      网友评论

        本文标题:有关MVP&MVI的一些事

        本文链接:https://www.haomeiwen.com/subject/bmhzkftx.html