Android multiple back stacks導航的幾種實現

Android multiple back stacks導航

談談android中多棧導航的幾種實現.

什麼是multiple stacks

當用戶在app裏切換頁面時, 會需要向後回退到上一個頁面, 頁面歷史被保存在一個棧裏.
在Android裏我們經常說"back stack".

有時候在app裏我們需要維護多個back stack, 比較典型的場景是bottom navigation bar或者側邊的drawer.

如果需求要求在切換tab的時候保存每個tab上的歷史, 這樣當用戶返回的時候還是返回到上次離開的地方, 這種就叫multiple stacks.

(與之對應的single stack行爲是返回之後回到了tab首頁.)

本文之後的內容都以bottom bar的多棧導航爲例.

multi-stack的需求

首先還是討論一下需求.

當bottom bar不支持多棧時, 當點擊切換底部tab, 再返回原來的tab, 所有在之上打開的頁面都會消失, 只有第一層(根)頁面會顯示.

這也是可以接受的, 甚至在material design裏面作爲Android平臺的默認行爲被提及: material design

但它同時也說了, 如果需要的話, 這個行爲是可以被改的.

如果你想保留用戶在上個tab看過的內容狀態, 很可能就需要做multi-stack, 每個tab上的棧是獨立退出, 分別保留的.

通常, 這還不是僅有的需求.

如果用戶點擊已選中的tab, 需要重置這個stack嗎?

需要定製轉場動畫嗎?

需要保留tab歷史嗎? 比如從tab A -> B -> C, 在C的根頁面back, 是想回到B還是回到home tab?

在bottom navigation的默認實現中(用Android Studio創建一個Bottom Navigation的新項目), 在非home tab的根節點, 點擊back, 總是先回到home tab, 再次back纔會退出app.
因爲這樣是符合固定start destination的原則的. 用戶在打開後和關閉前, 看到的是同一個頁面.

但是如果你有保存tab歷史的需求, 也可以考慮如何定製它.

當你更進一步地涉及到實現層面, 你會遇到更多實際操作的問題, 比如怎麼把一個詳情頁push到一個指定的棧, 如何pop destination.

讓我們列一下幾個需求點:

  • 維護多個棧.
  • 切換tab: 手動點擊tab或者其他tab內的交互. 比如dashboard跳轉到某個內容tab.
  • Push/pop destinations.
  • 重選(reselect)tab會重置該棧. (clear history.)
  • 轉場動畫
  • tab歷史.

技術背景

要進行導航的選型, 首先確定一下你的"destination"是什麼.

是composable還是fragment, 或者乾脆是View, 解決方案可能有很大的不同.

以這篇文章的scope來說, 我們就關注一個傳統的android app, 用Activity和Fragment實現.
所以bottom tab上的tab內容, 是不同Fragment.

Fragment lifecycle

爲什麼這裏要提一下Fragment的生命週期呢?

因爲fragment的生命週期和它的ViewModel緊密關聯, 進一步關係到了在導航過程中我們是否需要關注fragment的狀態恢復和刷新.

首先複習一下Fragment生命週期的回調: 什麼時候onDestroy會被調用?

  • replacetransaction沒有addToBackStack().
  • 當fragment被removed或者被popBackStack().

replacetransaction加上addToBackStack(), 舊的fragment會被壓入棧, 但它的生命週期只調用到onDestroyView().
當在它之上的其他fragment pop出來以後, 舊的這個fragment實例依然是同一個, 它重新顯示, 重新從onCreateView()開始走.

這是我們在single back stack下預期的行爲.

ViewModel的生命週期和Fragment是對齊的, 也即Fragment的onDestroy()調用時, ViewModel的onCleared()被調用.

在導航切換目的地時, 如果fragment被destroy了, 我們可以保存一些關注的變量在saved instance bundle或者SavedStateHandle裏, 用於之後的狀態恢復.
但是如果fragment沒有被destroy, 我們可以剩下不少力氣做這些狀態恢復.

所以理想的狀態是, 壓棧後的fragment實例不會被銷燬重建.

爲了比較不同的解決方案, 我把一些sample放在了一起: https://github.com/mengdd/bottom-navigation-samples

Jetpack navigation component

官網: https://developer.android.com/guide/navigation

即便在FragmentManager的文檔 裏, 也建議開發者使用jetpack的navigation library來處理app的navigation.

multiple back stack的支持是Navigation 2.4.0-alpha01Fragment 1.4.0-alpha01才加的.

試了下這個 demo,
代碼非常簡單, 我們基本什麼都不用做.

關於這裏面的思想可以看這篇文章: https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134

優點:

  • 最知名, 畢竟是官方的庫.
  • 支持類型安全的參數.
  • NavigationController支持pop到一個指定的destination.
  • 可以和Compose navigation庫一起使用.

缺點:

  • Multi-stack的支持: 當切換tab時, 前一個tab上的所有fragment都會被destroy, 當返回tab時棧內fragment會重建. 所以狀態會丟, 頁面可能會刷新.
  • 每個tab都需要是一個內嵌的navigation graph, 如果有一些common的destination, 需要include到每個graph中去. xml的navigation文件感覺很像一個大塊的樣板代碼.

FragmentManager

如果我們想做更多的定製, 我們可以考慮用FragmentManager的新APIs自己手動實現.

在文檔中doc 介紹的:

FragmentManager allows you to support multiple back stacks with the saveBackStack()
and restoreBackStack() methods. These methods allow you to swap between back stacks by saving one back stack and restoring a different one.

這是navigation component實現中實現多棧導航使用的方法.
所以也可以解釋爲什麼切tab的時候fragment都被銷燬了.

saveBackStack() works similarly to calling popBackStack() with the optional name
parameter: the specified transaction and all transactions after it on the stack are popped.
The difference is that saveBackStack() saves the state of all fragments in the popped transactions.

優點:

  • 精細控制, 開發者獲得更多控制, 也更明白到底是怎麼回事.
  • 如果我們當前項目沒有采用任何navigation library, 都是手動跳轉, 採用這種方法我們就不用考慮遷移navigation.

缺點:

  • 要寫很多fragment transaction的樣板代碼.
  • 和navigation components一樣: 多棧實現中在切換棧時, 在舊的tab上的Fragments會被銷燬, 返回時全部重建.

Enro

https://github.com/isaac-udy/Enro

對於多module的大型項目來說, 我很推薦這個庫, 它可以幫助我們解耦module間的依賴.

multi-stack的demo

優點:

  • 基於註解, 所以要寫的代碼很少, 導航使用很方便.
  • 多module項目解耦.
  • 傳類型安全的參數和返回結果都很容易.
  • 可以在ViewModel中獲取navigation handle, 獲取參數.
  • 支持Compose做節點.
  • 對Unit Test也有一個輔助測試的依賴.
  • multi-stack support: 保持了切換tab的時候fragment實例.

缺點:

  • 可能目前還不是很知名. 需要說服別人學和採用這個.
  • Fragment的multi-stack: 不能rest stack到根節點. (嘗試了一下定製這個行爲, 有點難).

Simple-stack

https://github.com/Zhuinden/simple-stack

這裏推薦一下這個庫作者的文章Creating a BottomNavigation Multi-Stack using child Fragments with Simple-Stack.
關於如何用simple-stack來做multi-stack.

最開始作者展示了一個不用任何庫, 僅用child fragments來實現的版本.

這是手動實現的另一種思想了.

後來才引入了用simple-stack做的demo
這是採用了原作者提供的sample, 比較簡單, 試了一下以後我發現可能還需要添加更多的代碼, 來做實際的應用.
比如詳情頁需要獲得某個tab的local stack的實例, 從而把自己push上去.

優點:

  • 作者在社區十分活躍, 有很多視頻和文章介紹simple-stack這個庫. 所以社區支持挺好.
  • multi-stack support: 保持了切換tab的時候fragment實例.
  • 支持控制和清空棧的歷史.
  • 有compose的擴展.

缺點:

  • 如果你的bottom bar當前是在activity的佈局裏, 你需要把bottom bar和相關的東西都挪進一個RootFragment, 作爲總的節點.
  • 作者提供的multi-stack sample還非常簡單, 需要寫更多的代碼來或者當前正確的棧來做push和pop操作. 不瞭解這個庫可能會寫得很醜.

其他庫

還有一些庫, 不是通用的navigation解決方案, 而只是爲多棧導航設計的小庫.
比如:

這些庫都自帶sample.

優點:

  • 實現簡單, 只用幾個類. 如果我們想定製我們可以用這個代碼.
  • 要改動的範圍可以限制在bottom navigation的部分, 而不是整體改變navigation方案.

缺點:

  • 這些庫都不是很出名, 有不再維護的風險.
  • 可能和其他的navigation方案不能兼容, 比如Navigation Components. 需要考慮整體.

總結

android (fragment實現) multi-stack navigation的可能解決方案:

方案 流行 整體方案 活躍 支持清空棧 Fragment被保存, 不被銷燬 支持Multi-modules Compose擴展
Jetpack Navigation Components 官方, 最出名 Yes Yes Yes No Yes Yes
Fragment Manager Android SDK - Yes Yes No No -
Enro Star: 188 Yes Yes No Yes Yes Yes
Simple Stack Star: 1.2k Yes Yes Yes Yes Yes Yes
Child Fragments Android SDK - Yes Yes Yes No -
JetradarMobile/android-multibackstack Star: 224 No No Yes No No -
DimaKron/Android-MultiStacks Star: 32 No Not sure Yes Yes No -

注意:

  • 整體方案: 表示該方案可以用於app整體的navigation解決方案, 而不僅僅是解決multi-stack的問題.
  • Fragment被保存, 不被銷燬: 當跳轉或者切tab時, 被壓入棧中的fragments不會被destroyed. 多棧支持的情況下, 儘管fragment被返回時都會被重建, 但是如果它不被銷燬, 我們就不需要做額外的工作來緩存狀態.

References:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章