乐趣区

关于android:Android-多返回栈技术详解

用户通过零碎返回按钮导航回去的一组页面,在开发中被称为返回栈 (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 开发库和它其中的 FragmentNavigatorDialogFragmentNavigator 中。相似的,与 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 这个层面更多的是实现细节,而不是您须要间接与之交互的内容。能够这么说,咱们曾经实现了将 FragmentNavigatorComposeNavigator 迁徙到新的 Navigator API 的工作,使其可能正确地保留和复原它们的状态,在这个层面上您无需再做任何额定工作。

在 Navigation 中启用多返回栈

如果您正在应用 NavigationUI,它是用于连贯您的 NavController 到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView (当初叫 NavigationRailView) 和 NavigationView,多返回栈是 默认启用 的。这就意味着联合 navigation-fragmentnavigation-ui 应用就能够。

NavigationUI API 是基于 Navigation 的其余公共 API 构建的,确保您能够精确地为自定义组件构建您本人的版本。保障您能够构建所需的自定义组件。启用保留和复原返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重载办法能够帮忙您指定 pop 操作保留状态或者指定 navigate 操作来复原之前已保留的状态。

比方,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的模式) 都能够应用咱们在与 底部导航栏集成 所介绍的雷同的技术,并且联合 saveStaterestoreState 属性一起调用 navigate():

onClick = {navController.navigate(screen.route) {
   // 当用户抉择子项时在返回栈中弹出到导航图中的起始目的地
   // 来防止太过臃肿的目的地堆栈
   popUpTo(navController.graph.findStartDestination().id) {saveState = true}

   // 当反复抉择雷同项时防止雷同目的地的多重拷贝
   launchSingleTop = true
   // 当反复抉择之前曾经抉择的项时复原状态
   restoreState = true
 }
}

保留状态,锁定用户

对用户来说,最令人丧气的事件之一便是失落之前的状态。这也是为什么 Fragment 用一整页来解说 保留与 Fragment 相干的状态,而且也是我十分乐于更新每个层级来反对多返回栈的起因之一:

  • Fragments (比方齐全不应用 Navigation Component): 通过应用新的 FragmentManager API,也就是 saveBackStackrestoreBackStack
  • 外围的 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,咱们会尽快解决。

退出移动版