• Android multiple back stacks导航的几种实现


    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:

  • 相关阅读:
    三维图形学课程笔记,3D建模与游戏开发方向(unity3d,c4d,3dsmax, maya的对比与联系)
    Python 解析器BeautifulSoup4
    MATLAB库函数resample(重采样函数)的C语言实现【姊妹篇2纯C语言实现】
    明日方舟游戏助手:一键完成日常任务 | 开源日报 No.233
    浅谈一下:Java当中的类和对象
    mybatis中按照时间搜索功能
    华为OD面试分享13(2024年)
    Matlab工具箱N-way的使用
    51nod 22.7暑假冲刺联训CSP-J 第一场II D题
    JDBC连接数据库
  • 原文地址:https://www.cnblogs.com/mengdd/p/solutions-of-android-multi-stack-navigation.html