用户通过零碎返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 “ 返回栈 ”,对多返回栈的反对是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中开始的。本文将为您开展多返回栈的技术详解。
零碎返回按钮的乐趣
无论您在应用 Android 全新的 手势导航 还是传统的导航栏,用户的 “ 返回 ” 操作是 Android 用户体验中要害的一环,把握好返回性能的设计能够使利用更加贴近整个生态系统。
在最简略的利用场景中,零碎返回按钮仅仅 finish 您的 Activity。在过来您可能须要覆写 Activity 的 onBackPressed() 办法来自定义返回操作,而在 2021 年您无需再这样操作。咱们曾经在 OnBackPressedDispatcher 中提供了 针对自定义返回导航的 API。实际上这与 FragmentManager 和 NavController 中 曾经 增加的 API 雷同。
这意味着当您应用 Fragments 或 Navigation 时,它们会通过 OnBackPressedDispatcher
来确保您调用了它们返回栈的 API,零碎的返回按钮会将您推入返回栈的页面逐层返回。
多返回栈不会扭转这个根本逻辑。零碎的返回按钮依然是一个单向指令 —— “ 返回 ”。这对多返回栈 API 的实现机制有深远影响。
Fragment 中的多返回栈
在 surface 层级,对于 多返回栈的反对 貌似很间接,但其实须要额定解释一下 “Fragment 返回栈 ” 到底是什么。FragmentManager 的返回栈其实蕴含的不是 Fragment,而是由 Fragment 事务组成的。更精确地说,是由那些调用了 addToBackStack(String name)) API 的事务组成的。
这就意味着当您调用 commit()
提交了一个调用过 addToBackStack()
办法的 Fragment 事务时,FragmentManager
会执行所有您在事务中所指定的操作 (比方 替换操作
),从而将每个 Fragment 转换为预期的状态。而后 FragmentManager
会将该事务作为它返回栈的一部分。
当您调用 popBackStack()
办法时 (无论是间接调用,还是通过零碎返回键以 FragmentManager
外部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 — 比方新增加的 Fragment 会被移除,暗藏的 Fragment 会显示。这会使得 FragmentManager
复原到最后提交 Fragment 事务之前的状态。
作者注: 这里有一个十分重要的事件须要大家留神,在同一个
FragmentManager
中相对不应该将含有addToBackStack()
的事务和不含的事务混在一起: 返回栈的事务无奈觉察返回栈之外的 Fragment 事务的批改 —— 当您从堆栈弹出一个十分不确定的元素时,这些事务从下层替换进去的时候会撤销之前未增加到返回栈的批改。
也就是说 popBackStack()
变成了销毁操作: 任何已增加的 Fragment 在事务被弹出的时候都会失落它的状态。换言之,您会失去视图的状态,任何所保留的实例状态 (Saved Instance State),并且任何绑定到该 Fragment 的 ViewModel 实例都会被革除。这也是该 API 和新的 saveBackStack()
办法之间的次要区别。saveBackStack()
能够实现弹出事务所实现的返回成果,此外它还能够确保视图状态、已保留的实例状态,以及 ViewModel 实例可能在销毁时被保留。这使得 restoreBackStack()
API 后续能够通过已保留的状态重建这些事务和它们的 Fragment,并且高效 “ 重现 ” 已保留的全副细节。太神奇了!
而实现这个目标必须要解决大量技术上的问题。
排除 Fragment 在技术上的阻碍
尽管 Fragment 总是会保留 Fragment 的视图状态,然而 Fragment 的 onSaveInstanceState() 办法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了可能保障调用 saveBackStack() 时 SavedInstanceState 会被保留,咱们 还 须要在 Fragment 生命周期切换 的正确机会注入对 onSaveInstanceState() 的调用。咱们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保留状态),也不能调用得太晚 (您须要在 Fragment 被销毁之前保留状态)。
这样的前提条件就开启了须要 解决 FragmentManager 转换到对应状态的问题,以此来保障有一个中央可能将 Fragment 转换为所需状态,并且解决可重入行为和 Fragment 外部的状态转换。
在 Fragment 的重构工作进行了 6 个月,进行了 35 次批改时,发现 Postponed Fragment 性能曾经重大损坏,这一问题使得被推延的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个批改和 5 个月的工夫里,咱们简直重写了 FragmentManager
治理状态、提早状态切换和动画的外部代码,具体请参见咱们之前的文章《全新的 Fragment: 应用新的状态管理器》。
Fragment 中值得期待的中央
随着技术问题的逐渐解决,包含更加牢靠和更易了解的 FragmentManager
,咱们新减少了两个 API: saveBackStack()
和 restoreBackStack()
。
如果您不应用这些新增 API,则一切照旧: 单个 FragmentManager
返回栈和之前的性能雷同。现有的 addToBackStack()
放弃不变 —— 您能够将 name
赋值为 null 或者任意 name
。然而,当您应用多返回栈时,name
的作用就十分重要了: 在您调用 saveBackStack()
和之后的 restoreBackStack()
办法时,它将作为 Fragment 事务的惟一的 key。
举个例子,会更容易了解。比方您曾经增加了一个初始的 Fragment 到 Activity,而后提交了两个事务,每个事务中蕴含一个独自的 replace 操作:
// 这是用户看到的初始的 Fragment
fragmentManager.commit {setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// 而后,响应用户操作,咱们在返回栈中减少了两个事务
fragmentManager.commit {setReorderingAllowed(true)
replace<ProfileFragment>(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {setReorderingAllowed(true)
replace<EditProfileFragment>(R.id.fragment_container)
addToBackStack(“edit_profile”)
}
也就是说咱们的 FragmentManager 会变成这样:
△ 提交三次之后的 FragmentManager 的状态
比如说咱们心愿将 profile 页换出返回栈,而后切换到告诉 Fragment。这就须要调用 saveBackStack()
并且紧跟一个新的事务:
fragmentManager.saveBackStack("profile")
fragmentManager.commit {setReorderingAllowed(true)
replace<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}
当初咱们增加 ProfileFragment
的事务和增加 EditProfileFragment
的事务都保留在 “profile” 关键字下。这些 Fragment 曾经齐全将状态保留,并且 FragmentManager
会伴随事务状态一起放弃它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager
中 —— 存在的仅仅只有状态 (以及任何以 ViewModel
实例模式存在的非配置状态)。
△ 咱们保留 profile 返回栈并且增加一个新的 commit 后的 FragmentManager 状态
替换回来非常简单: 咱们能够在 "notifications"
事务中同样调用 saveBackStack() 操作,而后调用 restoreBackStack()
:
fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)
这两个堆栈项高效地替换了地位:
△ 替换堆栈项后的 FragmentManager 状态
维持一个独自且沉闷的返回栈并且将事务在其中替换,这保障了当返回按钮被点击时,FragmentManager
和零碎的其余局部能够保持一致的响应。实际上,整个逻辑并未扭转,同之前一样,依然弹出 Fragment 返回栈的最初一个事务。
这些 API 都特意依照最小化设计,只管它们会产生潜在的影响。这使得开发者能够基于这些接口设计本人的构造,而无需通过任何非常规的形式保留 Fragment 的视图状态、已保留的实例状态、非配置的状态。
当然了,如果您不心愿在这些 API 之上构建您的框架,那么能够应用咱们所提供的框架进行开发。
应用 Navigation 将多返回栈适配到任意屏幕类型
Navigation Component 最后 是作为通用运行时组件进行开发的,其中不波及 View、Fragment、Composable 或者其余屏幕显示相干类型及您可能会在 Activity 中实现的 “ 目的地界面 ”。然而,NavHost 接口 的实现中须要思考这些内容,通过它增加一个或者多个 Navigator 实例时,这些实例 的确 分明如何与特定类型的目的地进行交互。
这也就意味着与 Fragment 的交互逻辑全副封装在了 navigation-fragment
开发库和它其中的 FragmentNavigator
与 DialogFragmentNavigator
中。相似的,与 Composable 的交互逻辑被封装在齐全独立的 navigation-compose
开发库和它的 ComposeNavigator
中。这里的形象设计意味着如果您心愿仅仅通过 Composable 构建您的利用,那么当您应用 Navigation Compose 时无需任何波及到 Fragment 的依赖。
该级别的拆散意味着 Navigation 中有两个档次来实现多返回栈:
- 保留独立的
NavBackStackEntry
实例状态,这些实例组成了NavController
返回栈。这是属于NavController
的职责。 - 保留 Navigator 针对每个
NavBackStackEntry
的特定状态 (比方与FragmentNavigator
目的地相关联的 Fragment)。这是属于Navigator
的职责。
仍需特地留神那些 尚未 更新的 Navigator
,它们无奈反对保留本身状态。底层的 Navigator
API 曾经整体重写来反对状态保留 (您须要覆写新增的 navigate()
和 popBackStack()
API 的重载办法,而不是覆写之前的版本),即便 Navigator
并未更新,NavController
仍会保留 NavBackStackEntry
的状态 (在 Jetpack 世界中向后兼容是十分重要的)。
备注: 通过绑定 TestNavigatorState 使其成为一个
mini-NavController
能够实现在新的Navigator
API 上更轻松、独立地测试您自定义的Navigator
。
如果您仅仅在利用中应用 Navigation,那么 Navigator
这个层面更多的是实现细节,而不是您须要间接与之交互的内容。能够这么说,咱们曾经实现了将 FragmentNavigator
和 ComposeNavigator
迁徙到新的 Navigator API 的工作,使其可能正确地保留和复原它们的状态,在这个层面上您无需再做任何额定工作。
在 Navigation 中启用多返回栈
如果您正在应用 NavigationUI,它是用于连贯您的 NavController
到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView
(当初叫 NavigationRailView
) 和 NavigationView
,多返回栈是 默认启用 的。这就意味着联合 navigation-fragment
和 navigation-ui
应用就能够。
NavigationUI
API 是基于 Navigation 的其余公共 API 构建的,确保您能够精确地为自定义组件构建您本人的版本。保障您能够构建所需的自定义组件。启用保留和复原返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions
上的新 API,也就是 navOptions
Kotlin DSL,以及 popBackStack()
的重载办法能够帮忙您指定 pop 操作保留状态或者指定 navigate 操作来复原之前已保留的状态。
比方,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的模式) 都能够应用咱们在与 底部导航栏集成 所介绍的雷同的技术,并且联合 saveState
和 restoreState
属性一起调用 navigate()
:
onClick = {navController.navigate(screen.route) {
// 当用户抉择子项时在返回栈中弹出到导航图中的起始目的地
// 来防止太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {saveState = true}
// 当反复抉择雷同项时防止雷同目的地的多重拷贝
launchSingleTop = true
// 当反复抉择之前曾经抉择的项时复原状态
restoreState = true
}
}
保留状态,锁定用户
对用户来说,最令人丧气的事件之一便是失落之前的状态。这也是为什么 Fragment 用一整页来解说 保留与 Fragment 相干的状态,而且也是我十分乐于更新每个层级来反对多返回栈的起因之一:
- Fragments (比方齐全不应用 Navigation Component): 通过应用新的
FragmentManager
API,也就是saveBackStack
和restoreBackStack
。 - 外围的 Navigation 运行时: 增加可选的新的
NavOptions
办法用于restoreState
(复原状态) 和saveState
(保留状态) 以及新的popBackStack
() 的重载办法,它同样能够传入一个布尔型的saveState
参数 (默认是 false)。 - 通过 Fragment 实现 Navigation:
FragmentNavigator
当初利用新的NavigatorAPI
,通过应用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。 NavigationUI
: 每当它们弹出返回栈时,onNavDestinationSelected
()、NavigationBarView.setupWithNavController()
和NavigationView.setupWithNavController()
当初默认应用 restoreState 和 saveState 这两个新的 NavOption。也就意味着 当降级到 Navigation 2.4.0-alpha01 或者更高版本后,任何应用 NavigationUI API 的利用无需批改代码即可实现多返回栈。
如果您心愿理解 更多应用该 API 的示例,请参考 NavigationAdvancedSample
(它是最新更新的,且不蕴含任何用于反对多返回栈的 NavigationExtensions
代码)。
对于 Navigation Compose 的示例,请参考 Tivi。
如果您遇到任何问题,请应用官网的问题追踪页面提交对于 Fragment 或者 Navigation 的 bug,咱们会尽快解决。