vitaviva / fragivity

Use Fragment like Activity
https://juejin.cn/post/6918693610359619592
MIT License
298 stars 40 forks source link

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

Open showwiki opened 3 years ago

showwiki commented 3 years ago

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

showwiki commented 3 years ago

在使用官方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 commented 3 years ago

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

showwiki commented 3 years ago

从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 commented 3 years ago

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

qdsfdhvh commented 3 years ago

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

showwiki commented 3 years ago

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

showwiki commented 3 years ago

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

showwiki commented 3 years ago

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

qdsfdhvh commented 3 years ago

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 commented 3 years ago

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

qdsfdhvh commented 3 years ago

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

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

showwiki commented 3 years ago

我参考了一下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 commented 3 years ago

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

showwiki commented 2 years ago

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

showwiki commented 2 years ago

这种修复 会触发这种异常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 commented 2 years ago

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

qdsfdhvh commented 2 years ago

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

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

qdsfdhvh commented 2 years ago

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

showwiki commented 2 years ago

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

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

qdsfdhvh commented 2 years ago

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

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

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

showwiki commented 2 years ago

@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.FragivityUtilActionPushToKt.pushToInternal$FragivityUtilActionPushToKt(ActionPushTo.kt:103) 6 com.github.fragivity.FragivityUtilActionPushToKt.pushToInternal$FragivityUtilActionPushToKt$default(ActionPushTo.kt:64) 7 com.github.fragivity.FragivityUtilActionPushToKt.pushTo(ActionPushTo.kt:41) 8 com.github.fragivity.FragivityUtil.pushTo(Unknown Source:1) 9 com.github.fragivity.FragivityUtilActionPushToKt.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 commented 2 years ago

@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 commented 2 years ago

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

showwiki commented 2 years ago

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

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