Android multiple back stacks導航 談談android中多棧導航的幾種實現. 什麼是multiple stacks 當用戶在app里切換頁面時, 會需要向後回退到上一個頁面, 頁面歷史被保存在一個棧里. 在Android里我們經常說"back stack". 有時候在app ...
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
會被調用?
- 當
replace
transaction沒有addToBackStack()
. - 當fragment被removed或者被
popBackStack()
.
當replace
transaction加上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實例不會被銷毀重建.
Navigation庫/可能的方案
為了比較不同的解決方案, 我把一些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-alpha01 和 Fragment 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 thesaveBackStack()
andrestoreBackStack()
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 callingpopBackStack()
with the optionalname
parameter: the specified transaction and all transactions after it on the stack are popped.
The difference is thatsaveBackStack()
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解決方案, 而只是為多棧導航設計的小庫.
比如:
- https://github.com/DimaKron/Android-MultiStacks
- https://github.com/JetradarMobile/android-multibackstack
這些庫都自帶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:
- Sample: https://github.com/mengdd/bottom-navigation-samples
- Jetpack Navigation Components
- Enro
- Simple-stack
出處: 博客園: 聖騎士Wind
Github: https://github.com/mengdd
微信公眾號: 聖騎士Wind