如何以编程方式更改导航图的起始目的地?

98

基本上,我有以下的导航图:

enter image description here

我想要在到达Fragment 2后将我的导航图起始点更改为它(以防止在按下返回按钮时回到fragment 1,就像启动屏幕一样)。

这是我的代码:

navGraph = navController.getGraph();
navGraph.setStartDestination(R.id.fragment2);
navController.setGraph(navGraph);

但是,显然它没有起作用,并且在按下后退按钮后返回到片段1

我做错了吗?还有其他解决方案吗?


在第二个片段中按下返回按钮后,您想要实现什么? - Denysole
我想在那种情况下退出应用程序。 - Shynline
2
可以通过编程方式更改导航图中的起始点。请参见此处以获取解决方案:https://dev59.com/lVQK5IYBdhLWcg3wZfB_ - Akash Patel
1
我甚至不知道你可以通过编程设置图表,谢谢,这个问题对我帮助很大! - dave o grady
1
不确定为什么你的代码不起作用。我在onCreate中设置它,没有问题。你在问题中提供的代码解决了我的问题 :) 谢谢! - Boy
问题中的代码在设置阶段(即包含NavHostFragment的Activity的onCreate()方法)中运行良好。 之前,我认为setStartDestination()就足够了,因为它操作原始的NavGraph实例。但显然只有调用setGraph()才能传播更改。 - Peter F
8个回答

99

更新:

当你有这样一个导航图:

<fragment
    android:id="@+id/firstFragment"
    android:name="com.appname.package.FirstFragment" >
    <action
        android:id="@+id/action_firstFragment_to_secondFragment"
        app:destination="@id/secondFragment" /> 
</fragment>

<fragment
    android:id="@+id/secondFragment"
    android:name="com.appname.package.SecondFragment"/>

如果您想导航到第二个片段并将其作为图形的根,请指定以下NavOptions:

NavOptions navOptions = new NavOptions.Builder()
        .setPopUpTo(R.id.firstFragment, true)
        .build();

并将它们用于导航:

Navigation.findNavController(view).navigate(R.id.action_firstFragment_to_secondFragment, bundle, navOptions);

setPopUpTo(int destinationId, boolean inclusive) - 通过弹出给定目标之前的所有非匹配目标来导航。这将从返回堆栈中弹出所有不匹配的目标,直到找到此目标。

destinationId - 要弹出并清除所有中间目标的目标。

inclusive - true表示从返回堆栈中也要弹出给定的目标。

<fragment
    android:id="@+id/firstFragment"
    android:name="com.appname.package.FirstFragment" >
<action
    android:id="@+id/action_firstFragment_to_secondFragment"
    app:destination="@id/secondFragment"
    app:popUpTo="@+id/firstFragment"
    app:popUpToInclusive="true" /> 
</fragment>

<fragment
    android:id="@+id/secondFragment"
    android:name="com.appname.package.SecondFragment"/>

然后在您的代码中:

findNavController(fragment).navigate(
    FirstFragmentDirections.actionFirstFragmentToSecondFragment())

旧答案

已弃用: clearTask 属性及其在 NavOptions 中的相关 API 已弃用。

来源:https://developer.android.com/jetpack/docs/release-notes


如果您想将根片段更改为 fragment 2(例如在按下 fragment 2 上的后退按钮后退出应用程序),您应该将下一个属性放入您的 actiondestination 中:

app:clearTask="true"

实际上,它看起来是这样的:

<fragment
    android:id="@+id/firstFragment"
    android:name="com.appname.package.FirstFragment"
    android:label="fragment_first" >
    <action
        android:id="@+id/action_firstFragment_to_secondFragment"
        app:destination="@id/secondFragment"
        app:clearTask="true" /> 
</fragment>

<fragment
    android:id="@+id/secondFragment"
    android:name="com.appname.package.SecondFragment"
    android:label="fragment_second"/>

我在操作中加入了 app:clearTask="true"


现在,当您从 片段 1 导航到 片段 2 时,请使用以下代码:

Navigation.findNavController(view)
        .navigate(R.id.action_firstFragment_to_secondFragment);

2
clearTask标志和setClearTask自alpha02以来已被弃用,请查看以下链接:https://dev59.com/91UL5IYBdhLWcg3wUmpR#50741496,https://dev59.com/nVUL5IYBdhLWcg3wd3tx。 - theme_an
这对我不起作用。你把这段代码放在哪里了?就像你一样,我有一个欢迎片段被设置为主页片段。在欢迎片段的onActivityCreated方法中,我获取我的视图模型,检查用户是否已登录,如果是,则尝试使用您上面提供的解决方案。但我仍然在第二个(新主页)片段上看到了向上箭头。我该如何去掉它? - szaske
我有点困惑。我在下面提交了我的代码解决方案。我知道你已经回答了自己的问题,但是我也遇到了同样的问题,而且你的解决方案只部分地解决了我的问题。提出另一种方法是否被认为是不礼貌的行为? - szaske
@desgraci 好的,我会检查当前的行为,并联系您提供一些答案。 - Denysole
1
请注意,如果您尝试从Fragment2执行findNavController.popBackstack以前往Fragment1,则不会按照预期工作,因为您已经实现了上述解决方案。上述解决方案仅适用于手动后退操作。popBackStack将返回false,并且您将无法离开Fragment2。 - Muhammad Ahmed AbuTalib
显示剩余6条评论

49

在 MainActivity.kt 文件中

    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val inflater = navHostFragment.navController.navInflater
    val graph = inflater.inflate(R.navigation.booking_navigation)

    if (isTrue){
        graph.startDestination = R.id.DetailsFragment
    }else {
        graph.startDestination = R.id.OtherDetailsFragment
    }

    val navController = navHostFragment.navController
    navController.setGraph(graph, intent.extras)

从 nav_graph.xml 中移除 startDestination

?xml version="1.0" encoding="utf-8"?>
<!-- app:startDestination="@id/oneFragment" -->

<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/navigation_main">

  <fragment
    android:id="@+id/DetailFragment"
    android:name="DetailFragment"
    android:label="fragment_detail"
    tools:layout="@layout/fragment_detail"/>
  <fragment
    android:id="@+id/OtherDetailFragment"
    android:name="OtherDetailFragment"
    android:label="fragment_other_detail"
    tools:layout="@layout/fragment_other_detail"/>
</navigation>

1
我遇到了一个“未定义起始目的地,错误通过app:startDestination”的错误。 - AndroidRuntimeException
12
您需要在活动的XML文件中删除图形标签“app:navGraph”。 - Jaffar Raza

22

我找到了解决方案,但它很丑陋。我想这是可以预料的,因为这是一个alpha库,但我希望Google能够简化/修复这个问题,因为这是一个相当流行的导航模式。

Alexey的解决方案对我没有用。我的问题是我在使用以下代码在Actionbar上显示向上箭头:

NavigationUI.setupActionBarWithNavController(this, navController)

如果我按照Alexey上面建议的做法去做,我的新起始片段仍然指向我的初始起始片段。如果我按那个向上箭头,我的应用程序会有点重新启动,转换到它本身(新起始片段)

以下是代码,可以实现我想要的效果:

  • 片段#1是我的应用程序最初开始的地方
  • 我可以在片段#1中进行认证检查,然后以编程方式将起始位置更改为片段#2。
  • 一旦进入片段#2,就没有向上箭头,并且按下返回按钮不会将您带回片段#1。

以下是实现此操作的代码。在我的Activity的onCreate中:

// Setup the toolbar
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(false)

// Configure the navigation
val navHost = nav_host_fragment as NavHostFragment
graph = navHost.navController
        .navInflater.inflate(R.navigation.nav_graph)
graph.startDestination = R.id.welcomeFragment

// This seems to be a magical command. Not sure why it's needed :(
navHost.navController.graph = graph

NavigationUI.setupActionBarWithNavController(this, navHost.navController)

还有:

fun makeHomeStart(){
    graph.startDestination = R.id.homeFragment
}

然后在片段#1的onActivityCreated方法中,根据Alexey的建议:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    ...

    // Check for user authentication
    if(sharedViewModel.isUserAuthenticated()) {

        (activity as MainActivity).makeHomeStart() //<---- THIS is the key
        val navOptions = NavOptions.Builder()
                .setPopUpTo(R.id.welcomeFragment, true)
                .build()
        navController.navigate(R.id.action_welcomeFragment_to_homeFragment,null,navOptions)

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

关键代码为:(activity as MainActivity).makeHomeStart(),它只是在活动中运行一个方法,该方法更改了图形的起始位置。我可以将其清理并转换为接口,但我会等待Google并希望他们改进整个过程。对我来说,“setPopUpTo”方法的命名似乎很糟糕,而且不直观,因为您正在命名从图形中剪切的片段。对我来说,他们在navOptions中进行这些更改也很奇怪。我认为navOptions只与它们连接的导航操作有关。

我甚至不知道navHost.navController.graph = graph是做什么的,但没有它,向上箭头就会返回。:(

我正在使用Navigation 1.0.0-alpha06。


我不确定为什么你被踩了,因为这是一个非常有用的答案,并且确实解决了OP的问题。由于您直接膨胀导航片段而不是通过通常的“findNavController”机制,因此需要完成奇怪的图形分配。我已经更新了您的帖子,提供了一种变体,而不是发布另一个答案。 - Carel
谢谢您的回答。现在有更好的解决方案吗? - Francis

15

您也可以尝试以下方法。

val navController = findNavController(R.id.nav_host_fragment)
if (condition) {
    navController.setGraph(R.navigation.nav_graph_first)
} else {
    navController.setGraph(R.navigation.nav_graph_second)
}

与其尝试弹出起始目标或手动导航到目标目的地,最好使用具有不同工作流程的另一个导航图。

当您需要有条件地完全不同的导航流程时,这将更加优秀。


你对我制作的这个演示有什么想法吗?我做出来了,但不知道正确的步骤是什么。它使用了BottomNavigationView和ViewPager。链接在这里:https://github.com/sunil-singh-chaudhary/Jetpac-Navigation-Activity-with-Fragments - Sunil Chaudhary
@SunilChaudhary 请参考此链接,其中包含有关如何更新UI的指南。https://developer.android.com/guide/navigation/navigation-ui,但在您的情况下,我认为使用BottomNavigationView设置viewpager并不是导航组件的一部分。 - Doctiger

5

您实际上不需要关闭闪屏片段,它可以一直存在于您的应用程序生命周期中。您应该在闪屏屏幕上确定下一个要显示的屏幕。

登录状态机流程

在上图中,您可以在Splash Screen状态中询问是否有保存的LoginToken。如果为空,则导航到登录屏幕。

一旦登录屏幕完成后,您就会分析结果并保存令牌,并导航到下一个片段主屏幕。

当在主屏幕中按下返回按钮时,您将向闪屏屏幕发送一个结果消息,指示其结束应用程序。

以下代码可能会有所帮助:

val nextDestination = if (loginSuccess) {
    R.id.action_Dashboard
} else {
    R.id.action_NotAuthorized
}

val options = NavOptions.Builder()
    .setPopUpTo(R.id.loginParentFragment, true)
    .build()

findNavController().navigate(nextDestination, null, options)

3

如果您有一个与此类似的导航XML文件:

<?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/mobile_navigation"
    app:startDestination="@+id/nav_home">

    <fragment
        android:id="@+id/nav_home"
        android:name="HomeFragment"
        android:label="@string/menu_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/nav_users"
        android:name="UsersFragment"
        android:label="@string/users"
        tools:layout="@layout/fragment_users" />

    <fragment
        android:id="@+id/nav_settings"
        android:name="SettingsFragment"
        android:label="@string/settings"
        tools:layout="@layout/fragment_settings" />
</navigation>

假设当前打开的片段是主页片段,您想要导航到用户片段,只需在您想要导航到的元素的setOnClickListener中调用nav控制器的navigate方法,类似于以下代码:
yourElement.setOnClickListener {
  view.findNavController().navigate(R.id.nav_users)
}

这将使应用程序导航到另一个片段,并且还将处理工具栏中的标题。


3

好的,在研究一段时间后,我找到了一个对我有效且不需要太多工作的解决方案。

似乎有两个条件必须满足,才能使其功能类似于您的secondFragment是起始目标。

使用接受帖子中的"ALTERNATIVE"选项。

<fragment
    android:id="@+id/firstFragment"
    android:name="com.appname.package.FirstFragment" >

    <action
        android:id="@+id/action_firstFragment_to_secondFragment"
        app:destination="@id/secondFragment"
        app:popUpTo="@+id/firstFragment"
        app:popUpToInclusive="true" /> 

</fragment>

<fragment
    android:id="@+id/secondFragment"
    android:name="com.appname.package.SecondFragment"/>

以上操作将从堆栈中移除firstFragment并在转移时填充secondFragment。 应用程序不能再返回到firstFragment,但是您可以像@szaske所述那样使用带有后退箭头的secondFragment。
这就是区别所在。我之前使用NavigationController.graph定义了我的AppBarConfig配置,如下所示:
// Old code
val controller by lazy { findNavController(R.id.nav_host_fragment) }
val appBarConfig by lazy { AppBarConfiguration(controller.graph) }

将其更新以定义一组顶级目标,纠正了在secondFragment上显示返回箭头而不是汉堡菜单图标的问题。

// secondFragment will now show hamburger menu instead of back arrow.
val appBarConfig by lazy { AppBarConfiguration(setOf(R.id.firstFragment, R.id.secondFragment)) }

设置起始目的地可能会对您的项目产生负面影响,因此根据需要进行设置。但在本示例中,我们不需要这样做。如果您希望确保图形具有正确定义的起始片段,则可以按如下方式执行。

controller.graph.startDestination = R.id.secondFragment

注意:设置此项并不会阻止在 secondFragment 中发生返回箭头,而且据我所知,似乎对导航没有影响。

你在什么时候/地方使用控制器和appBarConfig? - pokumars
@pokumars 当您在设置操作栏setupActionBarWithNavController(navController, appBarConfig)时,应该在您的活动中执行此操作。 - DevinM

1
我尝试修改startDestination代码,虽然它可以正常工作,但是导航组件无法恢复片段堆栈。
我通过使用一个虚拟的startDestination来解决了这个问题。
  • startDestination是一个空的Fragment(只是一个虚拟的)
  • EmptyFragmentFirstFragment的操作需要popUpTo=EmptyFragmentpopUpToInclusive=true

NavGraph image

Activity.onCreate()中:

    if (savedInstanceState == null) {
        val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
        val navController = navHost.findNavController()
        if (loginComplete) {
            navController.navigate(
                R.id.action_emptyFragment_to_FirstFragment
            )
        } else {
            navController.navigate(
                R.id.action_emptyFragment_to_WelcomeFragment
            )
        }
    }

当 Activity 被重新创建时,savedInstanceState 不为 null,片段会自动恢复。


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