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被返回時都會被重建, 但是如果它不被銷燬, 我們就不需要做額外的工作來緩存狀態.