导航popUpTo和PopUpToInclusive不能清除返回栈

75

我对Android Jetpack Navigation架构还不熟悉,正在尝试在新应用上使用。该应用只有一个活动和几个片段,其中两个是登录屏幕和电子邮件登录屏幕。我在我的导航XML中定义了这些片段。应用程序的流程如下:

登录屏幕电子邮件登录屏幕

我想要的是,在导航到电子邮件登录屏幕后,当我按返回键时,应用程序退出。也就是说,登录屏幕的后退堆栈被移除。我知道登录屏幕不应该这样工作,但我还在摸索中。

我遵循了Google的开始使用导航组件文档。它说,使用app:popUpToapp:popUpToInclusive="true"可以清除后退堆栈,但是当我在电子邮件登录屏幕上按返回键时,它仍然回到登录屏幕而没有退出应用程序。

所以,这是我尝试过的。

nav_main.xml

<fragment android:id="@+id/loginFragment"
          android:name="com.example.myapp.ui.main.LoginFragment"
          android:label="@string/login"
          tools:layout="@layout/fragment_login" >
    
    <action
        android:id="@+id/action_login_to_emailLoginFragment"
        app:destination="@id/emailLoginFragment"
        app:popEnterAnim="@anim/slide_in_right"
        app:popExitAnim="@anim/slide_out_right"
        app:popUpTo="@+id/emailLoginFragment"
        app:popUpToInclusive="true"/>

</fragment>

<fragment android:id="@+id/emailLoginFragment"
          android:name="com.example.myapp.ui.main.EmailLoginFragment"
          android:label="EmailLoginFragment"
          tools:layout="@layout/fragment_login_email" />

LoginFragment.kt

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    binding.emailLoginButton.setOnClickListener {
        findNavController().navigate(R.id.action_login_to_emailLoginFragment)
    }
    
    return binding.root
}

我给一个按钮添加了点击事件,在其中使用导航控制器通过给它的操作ID来导航到电子邮件登录屏幕。在 <action> 中,有 app:popUpToapp:popUpToInclusive="true"

反复阅读文档以及阅读大量 StackOverflow 问题后,我发现这些属性应该将我的登录屏幕从后堆栈中移除。但是它们没有起到作用。按钮确实导航到电子邮件登录屏幕,但当我按返回键时,它仍然返回到登录屏幕而不是退出应用程序。我漏掉了什么?


仅供记录。文档中指出,像您使用的那样使用登录片段是可以的。我不明白为什么“登录屏幕不应该以那种方式工作”。 - Panos Gr
2
@PanosGr,您是指“例如,如果您的应用程序具有初始登录流程,一旦用户已登录,您应该弹出所有与登录相关的目标,以便返回按钮不会将用户带回登录流程”吗?嗯,在我的情况下,用户尚未登录。它只是打开了另一种登录方法。通常,用户应该被允许返回到主登录屏幕并选择其他登录方法。 - adrilz
8个回答

82
<action
        android:id="@+id/action_login_to_emailLoginFragment"
        app:destination="@id/emailLoginFragment"
        app:popEnterAnim="@anim/slide_in_right"
        app:popExitAnim="@anim/slide_out_right"
        app:popUpTo="@+id/loginFragment"
        app:popUpToInclusive="true"/>

您的popUpTo返回到电子邮件登录页面,然后由于包括关系进行了弹出。如果您将popUpTo更改为登录片段,则也会导航回去并弹出,因为包括标志,这将导致您期望的行为。


25
我尝试理解这个答案,但我无法理解。有人可以更清楚地向我解释一下吗? - Fugogugo
69
当您创建一个动作时,可以添加popUpTo值。当您使用此动作导航到目标并返回导航时,导航将带您回到popUpTo片段,在上面的代码示例中,即loginFragment。设置popUpToInclusive =“true”告诉动作“当您返回导航时,请同时删除popUpTo中的片段”,因此它也将删除loginFragment。这有帮助吗? - Chris C.
3
对我而言可以工作,但仍有一个问题,即在B片段中我仍然看到“向上箭头按钮”,这是不符合预期的。 - murt
你能帮我看一下这个问题吗:https://dev59.com/7L7pa4cB1Zd3GeqPqAd8#64740896,谢谢。 - Sam Chen
感谢@ChrisC的澄清。popUpToInclusive很狡猾!如果从片段D -> 片段A,听起来像是弹出D和A之间的任何片段(B,C),但实际上并不是这个意思。它的意思是弹出A。 - Ali Kazi

55

我写这篇答案是为了那些还没有完全理解 popUpTo 工作方式的人,并希望它的示例能对某些人有所帮助,因为大多数网站上的导航示例都是重复的,不显示整个图片。

在任何 <action> 中,如果我们为 app:popUpTo 写入一个值,则表示我们想要在完成操作后从返回堆栈中删除一些片段,但是当操作完成时将删除哪些片段呢?

它的顺序是后进先出,所以:

  • 在最后一个片段和 popUpTo 中定义的片段之间的所有片段都将被删除。
  • 如果我们添加 app:popUpToInclusive="true",则还将删除定义在 popUpTo 中的片段。

例如: 考虑一个导航图中从 A 到 G 的片段如下:

A->B->C->D->E->F->G

我们可以从 A 转到 B,然后从 B 转到 C 等等。考虑以下两个动作:

  1. 一个动作 E->F 我们写:
<action
    ...
    app:destination="@+id/F"
    app:popUpTo="@+id/C"
    app:popUpToInclusive="false"/>
  1. 对于 F->G,我们写作:
<action
    ...
    app:destination="@+id/G"
    app:popUpTo="@+id/B"
    app:popUpToInclusive="true"/>

然后使用操作E->F从E到F后,最后一个片段(F)和C之间的片段(在E->FpopUpTo中定义)将被移除。这次片段C不会被移除,因为app:popUpToInclusive="false",所以我们的返回堆栈变为:

A->B->C->F(目前F在顶部)

现在,如果我们使用操作F->G去到片段G:

上一次片段(G)和B之间的所有片段(在F->GpopUpTo中定义)将被移除,但这次片段B也会被移除,因为在F->G操作中,我们写了app:popUpToInclusive="true"。所以返回堆栈变为:

A->G(目前G在顶部)


很好的LIFO解释 - 谢谢! - Ozzini
感谢出色的解释,但这让我担心的情况很多。 这个问题令人担忧的主要原因是 LIFO 队列(顺便说一句,我猜它是 LIFO,因为它与 onBackPressed 交互的方式,但 popUpTo 的行为并不是它成为 LIFO 的原因。)是动态构建的,在某种程度上这是好的,因为它赋予了开发者自由去任何地方。 所以问题在于循环导航:当重复目标被堆叠时会发生什么;popUpTo 只会从第一个插入到最后一个找到的最近 id 生效吗? - Delark
1
光明的一面是,似乎可以通过简单地交换它们来创建动态主页片段:popUpTo="Home" + popUpToInclusive = "true"。 - Delark
1
这是2023年,我想补充一下,我所说的不会起作用的原因是"startDestination"与其他项类型不同。 startDestination是类"NavGraph.class",而其他项则在"FragmentNavigator.Destination"下。 因此,Fragment A将无法用作“弹出”点来使用。 相反,您需要:FIRST => 在popUpTo中使用您的nav_graph id ...而不是您的startDestination app:popUpTo = "@id/nav_graph"。 如果您使用startDestination片段的id,它将无法正常工作。 SECOND => app:launchSingleTop="true",THIRD => app:popUpToInclusive="true" - Delark

33

以下这两行代码是实现这个技巧的关键:

如果您想从A走到B,并且希望完成A:

您需要使用以下操作调用B:

    <fragment
        android:id="@+id/fragmentA"            
        tools:layout="@layout/fragment_a">

        <action
            android:id="@+id/action_call_B"
            app:destination="@+id/fragmentB"
            app:popUpTo="@id/fragmentA"
            app:popUpToInclusive="true" />

    </fragment>

    <fragment
        android:id="@+id/fragmentB"
        tools:layout="@layout/fragment_b">


    </fragment>

如果你在片段中打印 log,你会看到在执行此操作后,fragmentA 被销毁了。


1
谢谢,我很感激你的帮助。但是Rito已经给出了答案,而且我认为你的答案基本上是一样的。 - adrilz
请记住,如果您有一个Fragment链A -> B -> C -> D,那么您需要在popUpTo操作参数中传递以下内容,以便在按下返回按钮时退出。 app:popUpTo =“@id / A”,app:popUpTo =“@id / B”,app:popUpTo =“@id / C” - kosiara - Bartosz Kosarzycki

17
你可以像这个答案一样在XML中实现它,或者你也可以以编程方式实现:
NavOptions navOptions = new NavOptions.Builder().setPopUpTo(R.id.loginRegister, true).build();
Navigation.findNavController(mBinding.titleLogin).navigate(R.id.login_to_main, null, navOptions);

2
我认为这与被接受的答案相同,但是以编程方式完成。不错,虽然。将来可能会用到它。 - adrilz
你能帮我解决这个问题吗:https://dev59.com/7L7pa4cB1Zd3GeqPqAd8,谢谢。 - Sam Chen

13

popUpTo 用于定义在按下返回键时要到达的位置。如果设置了popUpInclusive = true,导航将跳过该处(即popUpTo所指定的位置)。


我认为这是目前最好的解释。 - Nahro

10

假设您的应用程序有三个目标地点——A,B和C——以及从A到B,从B到C和从C返回A的操作。相应的导航图在以下图片中显示:

一个包含三个目标地点(A,B和C)的循环导航图

每次导航动作都会将目标地点添加到后退堆栈中。如果您通过此流程重复导航,则后退堆栈将包含多个每个目标地点的集合(A,B,C,A,B,C,A等)。为避免这种重复,您可以在从目标地点C到目标地点A的操作中指定app:popUpTo和app:popUpToInclusive,如下例所示:

<fragment
android:id="@+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="@layout/fragment_c">

<action
    android:id="@+id/action_c_to_a"
    app:destination="@id/a"
    app:popUpTo="@+id/a"
    app:popUpToInclusive="true"/>

到达目的地C后,返回栈中包含每个目的地(A、B、C)的一个实例。当返回到目的地A时,我们还弹出了A,这意味着我们在导航时从堆栈中删除了B和C。使用app:popUpToInclusive="true",我们也将第一个A推出堆栈,从而有效地清除它。请注意,如果您不使用app:popUpToInclusive,则返回栈将包含两个目的地A的实例。


3
请注意,本答案的内容是从官方文档复制而来。https://developer.android.com/guide/navigation/navigation-navigate#pop-example - Augusto Carmo

0

我曾经遇到过类似的问题,我的解决方法很简单。在导航图中,您必须指定起始屏幕。我的是:

app:startDestination="@id/webview"

它被称为起始目的地,这是用户打开应用程序时看到的第一个屏幕,也是用户退出应用程序时看到的最后一个屏幕

如果您不希望在退出应用程序时显示登录活动,请将其删除作为起始目的地,并使用您希望在您的情况下最后显示的片段,即电子邮件登录屏幕。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav"
    app:startDestination="@id/Email Login screen">

此外,请确保您在主活动代码中重写 onBackPressed() 方法,如下所示:
override fun onBackPressed() {
    finish()
    super.onBackPressed()
}

现在你已经将登录片段作为起始目的地移除了,当应用程序打开时,不明显会显示哪个片段。

添加一个方法来实现宿主活动中的逻辑,并从oncreate()调用它。在我的情况下,我创建了initContent()来处理这个逻辑。这是代码:

   override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val navHostFragment = supportFragmentManager
        .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    navController = navHostFragment.navController


    if (savedInstanceState == null) {
        initContent()
    }

}

private fun initContent() {
    if (isNetworkConnected()) {
        navController.navigate(R.id.webView)

    } else {
        navController.navigate(R.id.noInternetFragment)
    }
}

希望这能帮助到某个人。


0

Sample: A -> B -> A

FragmentB.kt

尝试弹出控制器的返回栈

private fun popBackStackToA() {
    if (!findNavController().popBackStack()) {
//            Call finish on your Activity
        requireActivity().finish()
    }
}

返回堆栈


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接