Android multiple back stacks導航的幾種實現

2022-06-25 06:00:52

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: