很老生常談的架構,看了一下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<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()
方法是presenter
將intent
和邏輯層綁定在一起。 很關鍵的函數,可以看個例子:
@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)));
綁定intent
與PublicSubject
,這里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
時, 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
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
這種狀態回置。其實跟作者一開始提的思路差不多。