使用 PopUpTo 导航但不知道您的根目的地是什么 - 导航架构组件
Navigate with PopUpTo but not knowing what you root destination is - Navigation Architecture Component
在某些情况下,使用导航架构组件,我们还可以通过这样的代码进行动态导航:
navController.navigate(R.id.cartFragment)
然后当我们需要从那个目的地导航到一个新的根目的地(返回导航将关闭应用程序)时,我们无法知道我们当前的根目的地是什么,所以我们不知道要设置什么popUpTo 目的地。
val navigationOptions = NavOptions.Builder().setPopUpTo(R.id.???, true).build()
navController.navigate(R.id.loginFragment, null, navigationOptions)
如果我们将它设置为不在后台堆栈中的目的地,那么我们会得到警告日志(来自popBackStackInternal
in NavController
):
"Ignoring popBackStack to destination *:id/splashFragment as it was not found on the current back stack"
发生这种情况是因为我们之前在流程中也有多个案例,我们设置 PopUpTo 如此依赖于流程我们有不同的根目的地。
我查看了 NavController
上的所有可用方法并尝试了对 mBackStack
的一些思考,但我无法找到清除后台堆栈的方法。
如何在不知道当前根目的地的情况下清除后台堆栈?
编辑:添加了导航图示例
<fragment
android:id="@+id/navigation_cart"
android:name="ProjectsFragment_"
android:label="Cart"
tools:layout="@layout/fragment_cart" />
<fragment
android:id="@+id/loginFragment"
android:name="LoginFragment_"
android:label="Login"
tools:layout="@layout/fragment_login">
<--! Dynamic navigation to either Consent or Cart based on state -->
<action
android:id="@+id/action_login_to_home"
app:destination="@id/navigation_cart"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_loginFragment_to_consentFragment"
app:destination="@id/consentFragment" />
<action
android:id="@+id/action_loginFragment_to_contactFragment"
app:destination="@id/contactFragment" />
</fragment>
<fragment
android:id="@+id/splashFragment"
android:name="SplashFragment_"
android:label="SplashFragment"
tools:layout="@layout/fragment_splash">
<--! Dynamic navigation to either Onboarding, Login or Cart based on state -->
</fragment>
<dialog
android:id="@+id/consentFragment"
android:name="ConsentFragment"
android:label="ConsentFragment"
tools:layout="@layout/fragment_consent" />
<fragment
android:id="@+id/onboardingIntroFragment"
android:name="OnboardingIntroFragment_"
android:label="OnboardingIntroFragment"
tools:layout="@layout/fragment_onboarding">
<action
android:id="@+id/action_onboardingIntroFragment_to_loginFragment"
app:destination="@id/loginFragment"
app:popUpTo="@id/onboardingIntroFragment"
app:popUpToInclusive="true" />
</fragment>
<dialog
android:id="@+id/contactFragment"
android:name="ContactFragment"
android:label="ContactFragment"
tools:layout="@layout/fragment_contact" />
关于上述来自 SplashFragment 的具体案例,我们将根据状态动态导航到 Onboarding、Login 或 Cart。如果是这样,我们导航到登录并在成功登录后动态导航到购物车。
在一般动态导航的两种情况下,我们都不知道我们的根目的地是什么,尽管无法正确设置 popUpTo。当从 Splash 调用时,它是 SplashFragment,当从 Login 调用时,它是 LoginFragment,或者在结帐的另一种情况下,它可能是 CartFragment 或作为根目标的另一个片段。
如何动态确定根目的地是什么,或者只是在导航过程中清除后台堆栈?
虽然这有点简化,因为我们的应用程序中有更多相同模式的案例。
编辑 2:根目标定义
我将 root destination 定义为 popUpTo 的目的地,包括清除整个 backstack,因为 backaction 将关闭应用程序。
例如,Splash -> Login 将使用 popUpTo 清除 splash,然后 Login to Cart 应该清除其余的 backstack,但现在根目标不是 Splash,而是 Login as Splash 在登录时弹出。
我们最终通过两种方式解决了这个问题。
1) 当我们可以从我们的动态导航位置找出当前的根目的地时,我们将该参数解析到我们的动态导航器中。
object Navigator {
fun showStartFragment(navController: NavController, @IdRes currentDestination: Int) {
val navigationOptions = NavOptions.Builder().setPopUpTo(currentDestination, true).build()
if (!Settings.Intro.onboardingCompleted) {
navController.navigate(R.id.onboardingIntroFragment, null, navigationOptions)
} else {
val isLoggedIn = Settings.User.loggedIn
if (isLoggedIn) {
MainActivity.shared?.mainStateHandler?.showNextFragment(navController, currentDestination)
} else {
navController.navigate(R.id.loginFragment, null, navigationOptions)
}
}
}
}
2) 但我们也遇到过异步端点响应可能导致用户必须注销、对过期令牌执行操作或用户已被阻止的情况。为了解决这个问题,我们破解了 NavController 以通过反射获取根目的地。
fun NavController.getRootDestination(): NavDestination? {
val mBackStack = NavController::class.java.getDeclaredField("mBackStack").let {
it.isAccessible = true
return@let it.get(this) as Deque<*>
}
for (entry in mBackStack) {
val destination = Reflector.getMethodResult(entry, "getDestination") as NavDestination
if (destination !is NavGraph) {
return destination
}
}
return null
}
object Reflector {
fun getMethodResult(`object`: Any, methodName: String): Any? {
return `object`.javaClass.getMethod(methodName).let {
it.isAccessible = true
return@let it.invoke(`object`)
}
}
}
在某些情况下,使用导航架构组件,我们还可以通过这样的代码进行动态导航:
navController.navigate(R.id.cartFragment)
然后当我们需要从那个目的地导航到一个新的根目的地(返回导航将关闭应用程序)时,我们无法知道我们当前的根目的地是什么,所以我们不知道要设置什么popUpTo 目的地。
val navigationOptions = NavOptions.Builder().setPopUpTo(R.id.???, true).build()
navController.navigate(R.id.loginFragment, null, navigationOptions)
如果我们将它设置为不在后台堆栈中的目的地,那么我们会得到警告日志(来自popBackStackInternal
in NavController
):
"Ignoring popBackStack to destination *:id/splashFragment as it was not found on the current back stack"
发生这种情况是因为我们之前在流程中也有多个案例,我们设置 PopUpTo 如此依赖于流程我们有不同的根目的地。
我查看了 NavController
上的所有可用方法并尝试了对 mBackStack
的一些思考,但我无法找到清除后台堆栈的方法。
如何在不知道当前根目的地的情况下清除后台堆栈?
编辑:添加了导航图示例
<fragment
android:id="@+id/navigation_cart"
android:name="ProjectsFragment_"
android:label="Cart"
tools:layout="@layout/fragment_cart" />
<fragment
android:id="@+id/loginFragment"
android:name="LoginFragment_"
android:label="Login"
tools:layout="@layout/fragment_login">
<--! Dynamic navigation to either Consent or Cart based on state -->
<action
android:id="@+id/action_login_to_home"
app:destination="@id/navigation_cart"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_loginFragment_to_consentFragment"
app:destination="@id/consentFragment" />
<action
android:id="@+id/action_loginFragment_to_contactFragment"
app:destination="@id/contactFragment" />
</fragment>
<fragment
android:id="@+id/splashFragment"
android:name="SplashFragment_"
android:label="SplashFragment"
tools:layout="@layout/fragment_splash">
<--! Dynamic navigation to either Onboarding, Login or Cart based on state -->
</fragment>
<dialog
android:id="@+id/consentFragment"
android:name="ConsentFragment"
android:label="ConsentFragment"
tools:layout="@layout/fragment_consent" />
<fragment
android:id="@+id/onboardingIntroFragment"
android:name="OnboardingIntroFragment_"
android:label="OnboardingIntroFragment"
tools:layout="@layout/fragment_onboarding">
<action
android:id="@+id/action_onboardingIntroFragment_to_loginFragment"
app:destination="@id/loginFragment"
app:popUpTo="@id/onboardingIntroFragment"
app:popUpToInclusive="true" />
</fragment>
<dialog
android:id="@+id/contactFragment"
android:name="ContactFragment"
android:label="ContactFragment"
tools:layout="@layout/fragment_contact" />
关于上述来自 SplashFragment 的具体案例,我们将根据状态动态导航到 Onboarding、Login 或 Cart。如果是这样,我们导航到登录并在成功登录后动态导航到购物车。
在一般动态导航的两种情况下,我们都不知道我们的根目的地是什么,尽管无法正确设置 popUpTo。当从 Splash 调用时,它是 SplashFragment,当从 Login 调用时,它是 LoginFragment,或者在结帐的另一种情况下,它可能是 CartFragment 或作为根目标的另一个片段。
如何动态确定根目的地是什么,或者只是在导航过程中清除后台堆栈?
虽然这有点简化,因为我们的应用程序中有更多相同模式的案例。
编辑 2:根目标定义 我将 root destination 定义为 popUpTo 的目的地,包括清除整个 backstack,因为 backaction 将关闭应用程序。 例如,Splash -> Login 将使用 popUpTo 清除 splash,然后 Login to Cart 应该清除其余的 backstack,但现在根目标不是 Splash,而是 Login as Splash 在登录时弹出。
我们最终通过两种方式解决了这个问题。
1) 当我们可以从我们的动态导航位置找出当前的根目的地时,我们将该参数解析到我们的动态导航器中。
object Navigator {
fun showStartFragment(navController: NavController, @IdRes currentDestination: Int) {
val navigationOptions = NavOptions.Builder().setPopUpTo(currentDestination, true).build()
if (!Settings.Intro.onboardingCompleted) {
navController.navigate(R.id.onboardingIntroFragment, null, navigationOptions)
} else {
val isLoggedIn = Settings.User.loggedIn
if (isLoggedIn) {
MainActivity.shared?.mainStateHandler?.showNextFragment(navController, currentDestination)
} else {
navController.navigate(R.id.loginFragment, null, navigationOptions)
}
}
}
}
2) 但我们也遇到过异步端点响应可能导致用户必须注销、对过期令牌执行操作或用户已被阻止的情况。为了解决这个问题,我们破解了 NavController 以通过反射获取根目的地。
fun NavController.getRootDestination(): NavDestination? {
val mBackStack = NavController::class.java.getDeclaredField("mBackStack").let {
it.isAccessible = true
return@let it.get(this) as Deque<*>
}
for (entry in mBackStack) {
val destination = Reflector.getMethodResult(entry, "getDestination") as NavDestination
if (destination !is NavGraph) {
return destination
}
}
return null
}
object Reflector {
fun getMethodResult(`object`: Any, methodName: String): Any? {
return `object`.javaClass.getMethod(methodName).let {
it.isAccessible = true
return@let it.invoke(`object`)
}
}
}