很老生常谈的架构,看了一下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
中如果当前activity
在destroy
时正在改变配置(同时不finish
)那就保存改Presenter
每个Activity|Fragment|View
会对应一个mosbyViewId
,这个id
与presenter
相绑定,所以在onSaveInstanceState
时需要保存这个id
,在onCreate
时恢复。
至于Presenter
在onCreate
时attach
,在onDestroy
时detach
,如果是旋转屏幕则会保存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
动作,onDestroyView
中detach
总结一下,分三种情况:
- 旋转屏幕 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
一开始没怎么看懂,还是举个例子
作者创建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
是做啥的,它也只是起到连接解耦合的作用。
BehaviorSubject对象作为业务逻辑和View层的“中继”
PublishSubject对象作为“中继”,View与Presenter
Presenter
初始化时创建viewStateBehaviorSubject = BehaviorSubject.create();
BehaviorSubject
是个什么鬼呢:
// observer will receive the "one", "two" and "three" events, but not "zero"
BehaviorSubject
对于BehaviorSubject
来说,subscribe
后会接受到前一个发射的item.
如果是第一次attachView
,那么会进行bindIntent
attachView
1.bindIntent
bindIntent()
方法是presenter
将intent
和逻辑层绑定在一起。 很关键的函数,可以看个例子:
@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
将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)));
绑定intent
与PublicSubject
,这里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
时, viewRelayConsumerDisposable
和intentDisposable
会dispose
,前者是VS->consumer
连接点,后者是意愿(View)与PublishSubject
的连接点
而在destroy
时:
@Override
@CallSuper
public void destroy() {
detachView(false);
if (viewStateDisposable != null) {
viewStateDisposable.dispose();
}
unbindIntents();
reset();
}
即viewStateObservable
与viewStateBehaviorSubject
的断连。同时在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
的抽象吧。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 - 软件架构