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

如何清空backstack再启动一个新fragment呢 #47

Open
showwiki opened this issue Jul 16, 2021 · 24 comments
Open

如何清空backstack再启动一个新fragment呢 #47

showwiki opened this issue Jul 16, 2021 · 24 comments
Labels
enhancement New feature or request

Comments

@showwiki
Copy link

showwiki commented Jul 16, 2021

遇到个问题,是如果我的任务栈已经叠加了两个fragment , 比如 A打开B, 此时回退可以看到A, 但如果此时想从B到C,那怎么清除在后台的A呢? 从B到C, popSelf 不能清除掉最下层的A,popTo C也不能清除A, 从B到C, pop 和push 连着调用 会导致fragment重影, 新开的C和A重叠在一起了。有什么办法可以清空任务栈后再启动一个新的fragment, 或者清空某个后退站底层的fragment后再压入一个。

@showwiki showwiki changed the title 如何清空叠加了二层以上的Fragment 如何清空backstack再启动一个新fragment呢 Jul 16, 2021
@showwiki
Copy link
Author

在使用官方demo的时候,我把 LaunchModeFragment 中 binding.btnPoptohome.setOnClickListener {
navigator.popTo(HomeFragment::class)
} 中的 navigator.popTo(HomeFragment::class) 改为 navigator.popTo(HomeFragment::class, true)

再点击HomeFragment 中的任何按钮都崩溃,java.lang.IllegalArgumentException: No destination with ID 0 is on the NavController's back stack. The current destination is null

@qdsfdhvh
Copy link
Collaborator

目前做不到,NavController相关的方法基本都没法对底栈做处理,也让替换第一个fragment的行为变得束手无策;
我本来想从beta版本中multi-back功能中找点路子来处理,但是最近研究下来这条路也走不通;
现在是想尝试提供一个新方法,把老的相关的东西全部清空再去启动新的fragment;
不过个人时间有限,欢迎一起研究&pr,尽早把这个老大难的问题解决了。

@qdsfdhvh qdsfdhvh added the enhancement New feature or request label Jul 17, 2021
@showwiki
Copy link
Author

showwiki commented Jul 17, 2021

从popTo入手,观察了一下popBackStackInternal的实现,发现他是搞了一个for循环popStack,我在fragivity官方demo里面的LaunchModeFragment 中binding.btnPoptohome.setOnClickListener 中实验了如下反射代码,实验中我多点了几个标准的LaunchModeFragment 放在popStack中,目测是可以清掉后台backstack中所有fragment,除了root的HomeFragment,不过我自己都觉得太野蛮了些,一路反射,最后也需要loadroot才能加载想要的页面。 还是有些问题,等明天再研究一下loadroot的实现

val navController = navigator.navController
            val declaredField =
                androidx.navigation.NavController::class.java.getDeclaredField("mBackStack")
            val declaredField2 =
                androidx.navigation.NavController::class.java.getDeclaredField("mNavigatorProvider")

            declaredField2.isAccessible = true
            declaredField.isAccessible = true

            val mNavigatorProvider = declaredField2.get(navController) as NavigatorProvider
            val mBackStack = declaredField.get(navController) as Deque<NavBackStackEntry>

            if (mBackStack.isEmpty()) {
                // Nothing to pop if the back stack is empty
//                return
            }
            val popOperations = mutableListOf<Navigator<*>>()
            val iterator: Iterator<NavBackStackEntry> = mBackStack.descendingIterator()
            var foundDestination = false
            while (iterator.hasNext()) {
                val destination = iterator.next().destination
                val navigator: Navigator<*> = mNavigatorProvider.getNavigator(
                    destination.navigatorName
                )
                popOperations.add(navigator)
            }
            var popped = false
            for (item in popOperations) {
                popped = if (item.popBackStack()) {
                    val entry: NavBackStackEntry = mBackStack.removeLast()
                    if (entry.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                        val declaredMethod =
                            androidx.navigation.NavBackStackEntry::class.java.getDeclaredMethod(
                                "setMaxLifecycle",
                                Lifecycle.State::class.java
                            )
                        declaredMethod.isAccessible = true
                        declaredMethod.invoke(entry, Lifecycle.State.DESTROYED)
                    }
                    true
                } else {
                    // The pop did not complete successfully, so stop immediately
                    break
                }
            }

            val declaredMethod =
                androidx.navigation.NavController::class.java.getDeclaredMethod(
                    "updateOnBackPressedCallbackEnabled")
            declaredMethod.isAccessible = true
            declaredMethod.invoke(navController)

            val  activity =  requireActivity() as MainActivity
            val navHostFragment = activity.findOrCreateNavHostFragment(R.id.nav_host)
            navHostFragment.loadRoot(HomeFragment::class)

@qdsfdhvh
Copy link
Collaborator

说到重复loadRoot,它其实也算是再次调用了NavController.setGraph,这个倒是navigation支持的,就是没有跳转动画;可以尝试先用这从这个方向适配出一个无动画的方案。

qdsfdhvh pushed a commit that referenced this issue Jul 20, 2021
@qdsfdhvh
Copy link
Collaborator

刚刚在develop分支里新开发了一个pushTo方法,可以试下效果。

@showwiki
Copy link
Author

showwiki commented Jul 22, 2021

完美,navigation 终于可以用了,看了一下改动的源码,很巧妙啊,kotlin的扩展方法原来这么牛掰,就是为写扩展库用的啊。我还傻乎乎的反射。

@showwiki
Copy link
Author

showwiki commented Aug 7, 2021

Utils 中如果this.java.name.hascode容易冲突的话,可以考虑this.java.canonicalName 这个 带完整包名的名字,应该不会有冲突

@showwiki
Copy link
Author

showwiki commented Aug 7, 2021

navigator.showDialog打开DialogFragment后, 关闭 是navigator.navigateUp,还是 navigator.pop(), 用navigator 这种方式打开后,以前用dialogfragment 实例实现的监听好像用不了吧。以前都是DialogFragment()一个实例instance,然后instance.setCustomeListener定义一些监听进行相关处理。navigator有相关的支持吗

@qdsfdhvh
Copy link
Collaborator

qdsfdhvh commented Aug 7, 2021

navigator.showDialog打开DialogFragment后, 关闭 是navigator.navigateUp,还是 navigator.pop(), 用navigator 这种方式打开后,以前用dialogfragment 实例实现的监听好像用不了吧。以前都是DialogFragment()一个实例instance,然后instance.setCustomeListener定义一些监听进行相关处理。navigator有相关的支持吗

dialong方面库里就实现了自动添加了node,其他都是原有navigation那一套,关闭应该就是NavController.popBackStack;
我个人建议dialog方面还是维持原本的用法,去使用navigation打开反而不自由了。

我看了下这篇文章关于java:Name和CanonicalName有什么区别?,感觉输出差不多,而canonicalName在某些情况下会返回null,所以我暂时还是先用name。

@showwiki
Copy link
Author

showwiki commented Aug 18, 2021

发现一个bug , A->B->C. C pushTo 到A, 再返回 ,B 、 C都还在啊, 虽然从stack 弹窗显示,都不在,但点击返回键确能退回去,是不是graph里面的逻辑没有清掉

@qdsfdhvh
Copy link
Collaborator

抱歉才恢复,我查看下问题

发现一个bug , A->B->C. C pushTo 到A, 再返回 ,B 、 C都还在啊, 虽然从stack 弹窗显示,都不在,但点击返回键确能退回去,是不是graph里面的逻辑没有清掉

qdsfdhvh added a commit that referenced this issue Aug 19, 2021
@showwiki
Copy link
Author

showwiki commented Aug 19, 2021

我参考了一下androidx.navigation.NavController#popBackStackInternal 中的方法,navigation自身的popTo 用的应该就是这个方法,他还是进行了一些细节处理,

在修改了一下Ext.kt 中的 internal fun NavController.clearBackStackEntry() 的方法如下,就可以了,但还是用到了反射。而且在多组件的情况下有时候会导致 一个问题,A->B->C ApushTo 到C ,会导致B的残像一直在,A要设置背景才能覆盖。


@JvmSynthetic
internal fun NavController.clearBackStackEntry() {
   
    val mNavigatorProviderField =
        androidx.navigation.NavController::class.java.getDeclaredField("mNavigatorProvider")
    val mViewModelField =
        androidx.navigation.NavController::class.java.getDeclaredField("mViewModel")

    mViewModelField.isAccessible = true
    mNavigatorProviderField.isAccessible = true
    val mNavigatorProvider = mNavigatorProviderField.get(this) as NavigatorProvider
    val mViewModel = mViewModelField.get(this) as NavControllerViewModel

    val popOperations = mutableListOf<Navigator<*>>()
    val iterator = mBackStack.descendingIterator()
    while(iterator.hasNext()) {
        val destination =  iterator.next().destination
        val navigator =  mNavigatorProvider.getNavigator<Navigator<*>>(destination.navigatorName)
        popOperations.add(navigator)
    }

    popOperations.forEach { item ->
        if(item.popBackStack()) {
            val entry = mBackStack.removeLast()
            if(entry.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)){
                entry.maxLifecycle = Lifecycle.State.DESTROYED
            }
            if(mViewModel != null) {
                mViewModel.clear(entry.mId)
            }
        } else return@forEach

    }

    val declaredMethod =
        androidx.navigation.NavController::class.java.getDeclaredMethod(
            "updateOnBackPressedCallbackEnabled")
    declaredMethod.isAccessible = true
    declaredMethod.invoke(this)
}

@qdsfdhvh
Copy link
Collaborator

qdsfdhvh commented Aug 19, 2021

应该不用,我大致看了下原因,主要是没处理FragmentManager的返回栈,pushTo以后navController返回栈虽然空了不会消费返回事件,但是传到了FragmentManager里因为FragmentManager里的返回栈没清空触发了FragmentManager.popBackxxxx。

@showwiki
Copy link
Author

showwiki commented Dec 3, 2021

pushTo 还是有新问题, 官方demo ,如果我从 SplashFragment -> pushTo HomeFragment -> push -> LaunchModeFragment -> 这个时候 再 pushTo 比如 CommFragment 页面 ,发现 HomeFragment 最多只会走到 onDestroyView 不会走 onDestory
@qdsfdhvh @vitaviva

@showwiki
Copy link
Author

showwiki commented Dec 6, 2021

这种修复 会触发这种异常moveToState 触发 FragmentManagerViewModel.setIsStateSaved 的时候引发空指针异常 @qdsfdhvh
: java.lang.RuntimeException: Unable to resume activity {com.superhexa.supervision/com.superhexa.supervision.app.presentation.NavHostActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.fragment.app.FragmentManagerViewModel.setIsStateSaved(boolean)' on a null object reference at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4612) at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4644) at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2174) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:236) at android.app.ActivityThread.main(ActivityThread.java:8170) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967) Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.fragment.app.FragmentManagerViewModel.setIsStateSaved(boolean)' on a null object reference at androidx.fragment.app.FragmentManager.dispatchResume(FragmentManager.java:3085) at androidx.fragment.app.Fragment.performResume(Fragment.java:3048) at androidx.fragment.app.FragmentStateManager.resume(FragmentStateManager.java:607) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:306) at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1647) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3128) at androidx.fragment.app.FragmentManager.dispatchResume(FragmentManager.java:3086) at androidx.fragment.app.Fragment.performResume(Fragment.java:3048) at androidx.fragment.app.FragmentStateManager.resume(FragmentStateManager.java:607) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:306) at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1647) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3128) at androidx.fragment.app.FragmentManager.dispatchResume(FragmentManager.java:3086) at androidx.fragment.app.FragmentController.dispatchResume(FragmentController.java:273) at androidx.fragment.app.FragmentActivity.onResumeFragments(FragmentActivity.java:458) at androidx.fragment.app.FragmentActivity.onPostResume(FragmentActivity.java:447) at androidx.appcompat.app.AppCompatActivity.onPostResume(AppCompatActivity.java:240) at android.app.Activity.performResume(Activity.java:8420) at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4602) at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4644)  at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)  at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2174)  at android.os.Handler.dispatchMessage(Handler.java:106)  at android.os.Looper.loop(Looper.java:236)  at android.app.ActivityThread.main(ActivityThread.java:8170)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967) 

@showwiki
Copy link
Author

@qdsfdhvh 这个补丁我应用后会crash,但demo 不会,那样更改的原理是啥?

@qdsfdhvh
Copy link
Collaborator

@qdsfdhvh 这个补丁我应用后会crash,但demo 不会,那样更改的原理是啥?

应该是有问题的,demo不会也许是场景比较简单;我也暂时没搞清楚哪个因素导致没执行onDestroy,我在fix里只是手动fragmentManager.moveToState来设置fragment状态,但是从crash看这个方法不太好,需要找到原因来真正解决它。

@qdsfdhvh
Copy link
Collaborator

@showwiki 试试看这个commit有没有修复问题,我初步查下来好像是Fragment.mMaxState的限制,导致fragment只能跑到onDestroyView

@showwiki
Copy link
Author

showwiki commented Dec 13, 2021

@showwiki 试试看这个commit有没有修复问题,我初步查下来好像是Fragment.mMaxState的限制,导致fragment只能跑到onDestroyView

@qdsfdhvh 目前测试正常,我再观察一段时间。Fragment.mMaxState 我看fragivity代码中没有任何地方限制只能到onDestroyView,怎么会被限制呢

@qdsfdhvh
Copy link
Collaborator

qdsfdhvh commented Dec 14, 2021

@showwiki 试试看这个commit有没有修复问题,我初步查下来好像是Fragment.mMaxState的限制,导致fragment只能跑到onDestroyView

@qdsfdhvh 目前测试正常,我再观察一段时间。Fragment.mMaxState 我看fragivity代码中没有任何地方限制只能到onDestroyView,怎么会被限制呢

这算是我的问题,用了一堆非公开的api,必然会影响到它原本的流程。

@showwiki
Copy link
Author

showwiki commented Dec 17, 2021

@qdsfdhvh 在bugly 上 有观察到 pushTo 会出现这种问题 , 不过是偶现的,我再观察一下吧

main(1)

java.lang.IllegalArgumentException

No destination with ID 0 is on the NavController's back stack. The current destination is null

解析原始
1
androidx.navigation.NavController.getBackStackEntry(NavController.java:1358)
2
androidx.navigation.NavController.getViewModelStoreOwner(NavController.java:1325)
3
com.github.fragivity.NodeSaverKt.getNodeSaver(NodeSaver.kt:22)
4
com.github.fragivity.NodeSaverKt.bridge(NodeSaver.kt:35)
5
com.github.fragivity.FragivityUtil__ActionPushToKt.pushToInternal$FragivityUtil__ActionPushToKt(ActionPushTo.kt:103)
6
com.github.fragivity.FragivityUtil__ActionPushToKt.pushToInternal$FragivityUtil__ActionPushToKt$default(ActionPushTo.kt:64)
7
com.github.fragivity.FragivityUtil__ActionPushToKt.pushTo(ActionPushTo.kt:41)
8
com.github.fragivity.FragivityUtil.pushTo(Unknown Source:1)
9
com.github.fragivity.FragivityUtil__ActionPushToKt.pushTo(ActionPushTo.kt:36)
10
com.github.fragivity.FragivityUtil.pushTo(Unknown Source:1)
11
com.superhexa.supervision.feature.profile.presentation.router.HexaRouter$Login.navigateToLogin(HexaRouter.kt:40)
12
com.superhexa.supervision.feature.profile.presentation.setting.SettingFragment.signOutSuccess(SettingFragment.kt:110)

@showwiki
Copy link
Author

showwiki commented Jan 6, 2022

@qdsfdhvh @vitaviva 不知道是不是pushTo 导致的问题,现在push 操作和 pushTo 操作总会偶现 上面这个错误,查了一下发现是 CreateNode.kt中的62行,val nodeSaver = nodeSaver 的时候 调用 get() = ViewModelProvider(getViewModelStoreOwner(graph.id))
.get(NodeSaverImpl::class.java) 的 时候,grap.id为 0 导致,但究竟grap.id为什么为0,怎么为0,现在还搞不清楚。

val graph = graph
val nodeSaver = nodeSaver

@showwiki
Copy link
Author

showwiki commented Jan 11, 2022

@qdsfdhvh @vitaviva 观察了一段时间发现,这个可能受系统回收策略影响,系统在锁屏,或者app切换到后台一段时间后再回来app,会回收掉一些页面,或者杀掉一些对象,这个时候重新生成的grah id就为空了 。不知道这个时候应该怎么处理。

@showwiki
Copy link
Author

@qdsfdhvh @vitaviva 最后发现是在activity 中 解决一个问题时,更改了activity的onSaveInstanceState的逻辑, 通过在
activity的onSaveInstanceState 中增加一个 savedInstanceState.putBundle("nav_state", navHostFragment.navController.saveState())

在onRestoreInstanceState 中增加 navHostFragment.navController.restoreState(savedInstanceState.getBundle("nav_state")) 解决了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants