使用 MVI 編寫響應式 APP — 第六部分 — 狀態恢復

pcdack發表於2019-02-21

使用 MVI 編寫響應式 APP—第六部分—狀態恢復

在前面部落格中,我們討論了 Model-View-Intent (MVI)和單項資料流的重要性。這極大的簡化了狀態恢復。這如何做到和為什麼能夠做到咧?我們將在這篇部落格討論。

我們在這篇部落格中將要關注兩種場景: 在記憶體中恢復狀態(例如螢幕的方向發生改變)和恢復一個「持續狀態」(從先前儲存在 Activity.onSaveInstanceState() 的 Bundle 中恢復)。

在記憶體中

這是一個簡單的情況。我們只需要讓我們的 RxJava 隨著時間的變化遵從安卓元件的生命週期(例如,Acitivity,Fragment 甚至是 ViewGroups)繼續發射新的資料。例如,Mosby(作者寫的一個庫) 的 MviBasePresenter 建立在一個 RxJava 流內部通過使用 PublishSubject 來管理 view 的意圖,和通過 BehaviorSubject 去渲染狀態到 view 上。我已經在第二部分結尾處描述這些實現細節。最重要的一點是 MviBasePresenter 是獨立與 view 生命週期的一個元件,因此一個 view 可以在 Presenter 中被分離和附著。在 Mosby 中只有當 view 永久銷燬 Presenter 才會被「摧毀」(垃圾回收)。這僅僅是 Mosby 的實現細節。你的 MVI 實現可能和這個完全不一樣。最重要的是這種元件比如 Presenter 需要獨立於 view 的生命週期。因為這樣它能夠簡單的處理 view 的附著和分離事件,無論何時 view 需要重新附著到 Presenter 我們只需簡單地呼叫 view.render(previousState) (因此 Mosby 用內部 BehaviorSubject 來處理)。這僅僅是如何解決螢幕方向的一種解決方案。它也可以工作在返回棧導航中,例如,Fragment 在返回棧中,我們如果從返回棧中返回,我們可以簡單的再次呼叫 view.render(previousState),並且,view 也會顯示正確的狀態。 事實上,狀態就算沒有 view 附著也可以被改變。因為 Presenter 的獨立於生命週期,並且保持 RxJava 狀態流在記憶體中。想象接收一個改變資料(狀態的一部分)的通知,沒有 view 附著。無論何時 view 被重新附著,最後的狀態(包括從通知中更新的資料)都會交給 view 去渲染。

持久化狀態

這種場景在 MVI 這種單向資料流模式下也很簡單。假設我們希望 View 的狀態 (比如 Activity)不僅存活在記憶體中,還能在程式死亡後被暫存。通常,在安卓中我們使用 Activity.onSaveInstanceState(Bundle) 來儲存那樣的狀態。在 MVP 或者 MVVM 中,你不需要使用 Model 來代表狀態(見 第一部分),與之不同的是, 在 MVI 中,View 有一個 render(state) 方法來記錄最新的狀態,這讓保持最後一個狀態變得容易。因此,顯然易見的是打包和儲存狀態到一個 bundle 下面,並且事後恢復它,例如:

class MyActivity extends Activity implements MyView {

  private final static KEY_STATE = "MyStateKey";
  private MyViewState lastState;

  @Override
  public void render(MyState state) {
    lastState = state;
    ... // update UI widgets
  }

  @Override
  public void onSaveInstanceState(Bundle out){
    out.putParcelable(KEY_STATE, lastState);
  }

  @Override
  public void onCreate(Bundle saved){
    super.onCreate(saved);
    MyViewState initialState = null;
    if (saved != null){
      initialState = saved.getParcelable(KEY_STATE);
    }

    presenter = new MyPresenter( new MyStateReducer(initialState) ); // With dagger: new MyDaggerModule(initialState)
  }
  ...
}
複製程式碼

我知道你已經掌握了要點。請注意在 onCreate() 方法中我們不能直接呼叫 view.render(state),取而代之,我們應該讓初始化狀態下沉到狀態管理的地方:狀態摺疊器(看第三部分)在這裡我們用 .scan(initialState,reducerFunction)

結論

隨著單向資料流和一個 Model 代表一種狀態,很多與狀態相關的事情,變得相對於其他的模式更加簡單。然而,通常在我的 APP 中,我不會持久化狀態到 bundle 有以下兩點原因:第一, Bundle 有大小限制,因此你不能存很大的狀態在 bundle 中(相反,你需要儲存狀態到檔案,或者,儲存到物件儲存例如 Realm)。第二,我們僅僅討論瞭如何去序列化和反序列化,但是,這不一定與恢復狀態相同。

例子:讓我們假設我們有一個 LCE(Loading-Content-Error) 的 view,這個 view 在載入資料時會顯示一個指示器,並且當資料載入完成後會展示一個列表檢視。。因此,這個狀態應當是 MyViewState.LOADING。讓我們假設載入需要消耗一定的時間,就在載入時候,Activity 程式也被殺掉了(例如,因為其他應用程式佔據了前臺,像電話 app 因為女票的電話而佔據了前臺)。如果如之前所述,我們僅僅只是序列化了 MyViewState.LOADING 這個狀態,並在 Activity 被重新建立時反序列化它,那麼我們的狀態摺疊器只會去呼叫 view.render(MyViewState.LOADING) 。注意到目前為止一切都還好,但是接下來我們會發現去載入資料本身這個操作(發起一次 http 請求)永遠不會執行。這就是盲目簡單地反序列化帶來的結果。

正如你所見, 序列化與反序列化狀態,並不同於狀態恢復。狀態恢復也許需要一些新增額外的一些會增加複雜性的步驟(使用 MVI 來實現比我目前為止所使用的任何其他架構更加簡單)。當 view 被重新建立的時候,反序列化狀態也許包含了一些過期資料。因此,你需要想盡辦法更新資料。在大多數 app 中,我通過努力找到了一種更簡單和更加友好的方法,僅僅保持狀態到記憶體中。並且當程式死亡的時候,開啟一個空的初始化狀態就像 app 第一次啟動一樣。理想情況下一個 app 有快取和離線支援,因此當程式死亡,重新載入資料是很快的。

這最終導致我與其他安卓開發者都爭論過一個問題:如果我使用了快取或者儲存,那麼我就已經擁有了一個獨立於安卓生命週期之外的元件,我也不再需要去處理相關的狀態快取問題,MVI 完全就是在胡說嘛!對麼? 大多數這些安卓開發者推薦 Mike Nakhimovich 發表的Presenter 不是為了持久化這篇文章介紹的 NyTimes Store,一個資料載入和快取庫。不幸的是,這些這些開發者不理解載入資料和快取不是狀態管理。例如,如果我不得不從兩個快取或儲存中載入資料呢?

最後,像 NyTimes 緩衝庫幫助我們處理程式死亡了麼?很顯然沒有,因為程式死亡隨時可能發生。為來解決這個問題,我們能做的僅僅是乞求安卓作業系統不要殺死我們的 app 程式,因為我們依舊需要做一些工作通過安卓的 service (這個元件也是獨立於其他安卓元件的生命週期)或者我們現在用 rxjava 來取代 service,我們可以這樣麼?我們討論關於安卓的 service,rxjava 和 MVI 在下一部分。敬請期待(๑˙ー˙๑)。

劇透: 我認為我們需要 service。

這篇部落格是 “用 MVI 開發響應式App”中的一篇部落格。下面是內容表:

這是中文翻譯:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章