Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation in Android (五)——多返回栈技术 #137

Open
soapgu opened this issue Apr 21, 2022 · 0 comments
Open

Navigation in Android (五)——多返回栈技术 #137

soapgu opened this issue Apr 21, 2022 · 0 comments
Labels
problem problem or trouble 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Apr 21, 2022

  • 前言

在学习Navigation的时候就已经注意到多返回栈技术了,首先多返回栈技术是非常非常新,最近的更新的feature。但是一时半会没法消化理解,加上一时没真正应用上,所以暂时搁置了。想不到经过一次导航的bug,我开始重新审视“多返回栈技术”。

  • 诡异的bug

  • 描述
    图片
  1. 界面1,界面2,界面3分别是下方三个菜单的首页
  2. 界面1里面有个到界面2的跳转
  3. 点击下方按钮跳转一切正常
  4. 但是从界面1点内部的按钮跳转,完成后进入了一个“新开”的界面2
  5. 点击下面图标也无法回界面,仍然显示界面2
  6. 只有点击返回才可以回到界面1

到底是什么原因那

  • 我想象中的fragment 堆栈

图片
似乎有点解释无力
因为,我在一级菜单下的页面是会“缓存”下来的,而在一级页面下新开的页面又是会“压栈”操作。
用常规的堆栈模型显然是自相矛盾的!

  • 实际中的 堆栈

是的,你想象的没错,这里已经使用的“多堆栈技术”

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

先上图来理解下多堆栈下的情况

图片

  • 实际的堆栈仍然只有一个当前堆栈,有点像UI层的主线程

  • saveBackStack和restoreBackStack的api可以实现堆栈的瞬间存储和恢复,产生逻辑上的多堆栈

  • 其实每个一级菜单形成自己的堆栈,可以理解为“堆栈自治”

  • 以后我们的堆栈形成了一个二维平面展开的视图,所以说多堆栈技术实际就是一维升二维的技术,也算是谷歌的一个很重大的一个功能提升

  • bug修复过程

  • 1号方案

这个bug产生的原因我把两个一级页面的跳转当成了“普通”跳转。
目前点击下面导航按钮是正常的。所以“保守”的思路是,把跳转的行为做成菜单点击“一模一样”

底部导航栏提到了

NavigationUI 也可以处理底部导航。当用户选择某个菜单项时,NavController 会调用 onNavDestinationSelected() 并自动更新底部导航栏中的所选项目。

所以onNavDestinationSelected这个api是关键。
问题:
我在子页面中拿不到菜单项怎么办那,只能通过fragment 通信转一道手了

this.viewModel.getShowSpace().observe(this.getViewLifecycleOwner(), show -> {
            if (show) {
                Bundle result = new Bundle();
                result.putInt("menu", R.id.spaceFragment );
                getParentFragmentManager().setFragmentResult("requestMenuClick", result);
            }
        });

所以在子页面跳转部分的实现代码,把自己要跳转的id通过FragmentResult传出去

 NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager().findFragmentById(R.id.fragmentContainerView);
        assert navHostFragment != null;
        NavController navController = navHostFragment.getNavController();
        navHostFragment.getChildFragmentManager().setFragmentResultListener("requestMenuClick", this, (requestKey, bundle) -> {
            Logger.i( "------requestMenuClick-----" );
            MenuItem item = bottomNav.getMenu().findItem(bundle.getInt("menu"));
            Logger.i( "get menu item:%s" , item.getTitle()  );
            NavigationUI.onNavDestinationSelected( item , navController );
        });
        bottomNav = view.findViewById(R.id.bottom_nav);
        NavigationUI.setupWithNavController(bottomNav, navController);
        navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
            int id = destination.getId();
            if (mainResourceIds.contains(id)) {
                bottomNav.setVisibility(View.VISIBLE);
            } else {
                bottomNav.setVisibility(View.GONE);
            }
        });

这里导航的宿主页面,持有了BottomNavigationView和NavHostFragment
只要收到导航中的Fragment的Result,通过findItem找到对应的菜单项,“模拟”点击了一级菜单
结果很完美
这是保底方案实现了,还需要有更好的追求

  • 2号方案

首先看看1号方案实际做了点什么

@JvmStatic
    public fun onNavDestinationSelected(item: MenuItem, navController: NavController): Boolean {
        val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true)
        if (
            navController.currentDestination!!.parent!!.findNode(item.itemId)
            is ActivityNavigator.Destination
        ) {
            builder.setEnterAnim(R.anim.nav_default_enter_anim)
                .setExitAnim(R.anim.nav_default_exit_anim)
                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
                .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
        } else {
            builder.setEnterAnim(R.animator.nav_default_enter_anim)
                .setExitAnim(R.animator.nav_default_exit_anim)
                .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
                .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
        }
        if (item.order and Menu.CATEGORY_SECONDARY == 0) {
            builder.setPopUpTo(
                navController.graph.findStartDestination().id,
                inclusive = false,
                saveState = true
            )
        }
        val options = builder.build()
        return try {
            // TODO provide proper API instead of using Exceptions as Control-Flow.
            navController.navigate(item.itemId, null, options)
            // Return true only if the destination we've navigated to matches the MenuItem
            navController.currentDestination?.matchDestination(item.itemId) == true
        } catch (e: IllegalArgumentException) {
            false
        }
    }
  1. setLaunchSingleTop(true)
    防止请求实例重复,保证只有一个,这个比较好理解

  2. setRestoreState(true)和saveState = true
    这个和多堆栈相关,支持调用saveBackStack和restoreBackStack魔法

  3. setPopUpTo
    退栈操作,把堆栈清理干净,只留下首页

看到这里的魔法,那么我们普通导航也是可以做到的
图片
新增popUpTo,popUpToSaveState,launchSingleTop,restoreState4个属性
把里面的值设成和源码“一模一样”

运行一下,完美

  • 复盘下bug

初始状态
图片
从页面1点击普通导航到页面2
图片
这样界面2就不是一个独立的堆栈,而是附属于界面1的堆栈了所以会出现两个诡异的现象

  1. 点击下面界面1菜单,显示界面2的内容
    因为点击界面1的菜单是恢复了界面1的堆栈,所以显示当前最外层的界面2,从多堆栈逻辑来说是“没毛病”

  2. 界面2出现了两个实例,都是分别初始化
    界面2就出现了2个实例,一个是下方导航创建的独立堆栈,一个是界面1创建的界面2。很魔幻

感觉下来Navigation已经把堆栈的操作处理得越来越好了,其他这个组件也帮我们处理好fragmentmanager的管理,不需要我们对这一层亲自动手。
不过我这里还有一点隐忧

  • 不过多堆栈技术会不会很吃内存啊,其实保存过程中ViewModel都是保持状态的。
  • 不同模块的页面的耦合性不得不增加,如果都加到一个导航图里面了,那么耦合度也没法解了,暂时只能先这样了。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
problem problem or trouble 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant