Android多返回栈技术详解


/   今日科技快讯   /

近日,字节跳动正式宣布开源CloudWeGo,这是一套以Go语言为核心、专注于微服务通信与治理的项目集合。基于字节跳动基础架构团队构建分布式系统的成功实践,CloudWeGo具有高性能、可扩展、高可靠的特点。在抖音等App亿级流量背后,字节跳动基础架构团队开发的技术底座支撑着庞大的微服务生态系统。从2018年至今,该团队维护的在线微服务数量增长了近600%,已达到5万的规模。CloudWeGo也在此过程中持续迭代和完善。

/   前言   /

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈(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中值得期待的地方   /

随着技术问题的逐步解决,包括更加可靠和更易理解的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。示例地址如下所示:

https://github.com/chrisbanes/tivi

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

官方推荐Flow,LiveData:那我走?

PermissionX 1.5发布,支持申请Android特殊权限啦

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值