談談android中多棧導航的幾種實現.
當用戶在app裡切換頁面時, 會需要向後回退到上一個頁面, 頁面歷史被儲存在一個棧裡.
在Android裡我們經常說"back stack".
有時候在app裡我們需要維護多個back stack, 比較典型的場景是bottom navigation bar或者側邊的drawer.
如果需求要求在切換tab的時候儲存每個tab上的歷史, 這樣當用戶返回的時候還是返回到上次離開的地方, 這種就叫multiple stacks.
(與之對應的single stack行為是返回之後回到了tab首頁.)
本文之後的內容都以bottom bar的多棧導航為例.
首先還是討論一下需求.
當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.
讓我們列一下幾個需求點:
要進行導航的選型, 首先確定一下你的"destination"是什麼.
是composable還是fragment, 或者乾脆是View, 解決方案可能有很大的不同.
以這篇文章的scope來說, 我們就關注一個傳統的android app, 用Activity和Fragment實現.
所以bottom tab上的tab內容, 是不同Fragment.
為什麼這裡要提一下Fragment的生命週期呢?
因為fragment的生命週期和它的ViewModel緊密關聯, 進一步關係到了在導航過程中我們是否需要關注fragment的狀態恢復和重新整理.
首先複習一下Fragment生命週期的回撥: 什麼時候onDestroy
會被呼叫?
replace
transaction沒有addToBackStack()
.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範例不會被銷燬重建.
為了比較不同的解決方案, 我把一些sample放在了一起: https://github.com/mengdd/bottom-navigation-samples
官網: 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
優點:
缺點:
如果我們想做更多的客製化, 我們可以考慮用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.
優點:
缺點:
https://github.com/isaac-udy/Enro
對於多module的大型專案來說, 我很推薦這個庫, 它可以幫助我們解耦module間的依賴.
multi-stack的demo
優點:
缺點:
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上去.
優點:
缺點:
還有一些庫, 不是通用的navigation解決方案, 而只是為多棧導航設計的小庫.
比如:
這些庫都自帶sample.
優點:
缺點:
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 | - |
注意: