有关MVP&MVI的一些事

很老生常谈的架构,看了一下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

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

有关MVP&MVI的一些事_第1张图片
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是做啥的,它也只是起到连接解耦合的作用。

有关MVP&MVI的一些事_第2张图片
image

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

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

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

 // observer will receive the "one", "two" and "three" events, but not "zero"
  BehaviorSubject 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> selectedItemsIntent =
        intent(ShoppingCartOverviewView::selectItemsIntent)
            .mergeWith(clearSelectionIntent.map(ignore -> Collections.emptyList()))
            .doOnNext(items -> Timber.d("intent: selected items %d", items.size()))
            .startWith(new ArrayList(0));


    subscribeViewState(selectedItemsIntent, ShoppingCartOverviewView::render);
  }

intent:

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

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

PublishSubject 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 viewStateObservable,
      @NonNull ViewStateConsumer 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() {
    @Override
    public void accept(VS vs) throws Exception {
        viewStateConsumer.accept(view, vs);
    }
});

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

3.绑定意愿
Observable 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(intentRelay)));

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

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

Reducer

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

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

Observable 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。

有关MVP&MVI的一些事_第3张图片
image.png

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

这只是一种思想:

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

issue

  • https://github.com/sockeqwe/mosby/issues/261
 Observable 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 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的一些事)