有關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

一開始沒怎么看懂,還是舉個例子

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 - 軟件架構

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容