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

How to change start destination of a navigation graph programmatically?

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

我想在到达后立即将导航图中的起点更改为 fragment 2(以防止在按下后退按钮时返回到 fragment 1 - 就像启动画面一样)。

这是我的代码:

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

但是,显然它不起作用,在按下后退按钮后它返回 fragment 1

我做错了吗? 还有其他解决办法吗?

更新:

当你有这样的导航图时:

<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) - Pop up to a given destination before navigating. This pops all non-matching destinations from the back stack until this destination is found.

destinationId - The destination to pop up to, clearing all intervening destinations.

inclusive - true to also pop the given destination from the back stack.


备选方案:

<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" 添加到操作中。


现在,当您执行从 fragment 1fragment 2 的导航时,请使用下一个代码:

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

我找到了解决这个问题的方法,但它很难看。我想这在 alpha 库中是意料之中的,但我希望 Google 研究 simplifying/fixing 因为这是一种非常流行的导航模式。

Alexey 的解决方案对我不起作用。我的问题是我的 Actionbar 上显示了向上箭头,方法是:

NavigationUI.setupActionBarWithNavController(this, navController)

如果我按照 Alexey 上面的建议进行操作,我的新起始片段仍然有一个指向我的初始起始片段的箭头。如果我按下向上箭头,我的应用程序会重新启动,过渡到自身(新的开始片段)

这是达到我想要的效果所需的代码:

  • 片段 #1 是我的应用程序最初开始的地方
  • 我可以在片段 #1 中进行身份验证检查,然后以编程方式将开头更改为片段 #2。
  • 在 Fragment #2 中没有向上箭头,按后退按钮不会将您带到 Fragment #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 中Activity创建,根据 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() 只是在 activity 中运行一个方法来更改图形的 startDestination。我可以清理它并将其变成一个界面,但我会等待 Google 并希望他们改进整个过程。方法 'setPopUpTo' 对我来说似乎命名不当,而且您命名从图中切出的片段并不直观。他们在 navOptions 中进行这些更改对我来说也很奇怪。我认为 navOptions 只会与它们所连接的导航操作有关。

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

我正在使用 Navigation 1.0.0-alpha06。

您真的不需要弹出 Splash Fragment。它可以在您的 App 余生中一直保留在那里。您应该做的是从初始屏幕确定要显示的下一个屏幕。

上图可以在Splash Screen State询问是否有保存的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)

您也可以尝试以下方法。

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)
}

与其尝试弹出起始目的地或手动导航到目标目的地,不如使用另一个具有不同工作流程的导航图会更好。 当您有条件地想要完全不同的导航流程时,这会更好。

在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>

好吧,在稍微弄乱了这个之后,我找到了一个不需要大量工作的适合我的解决方案。

似乎必须有两件事才能发挥作用,就像您的 secondFragment 是您的起始目的地一样。

在已接受的 post

中使用 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,但您的左侧 secondFragment 显示了@szaske 所述的后退箭头。

这就是不同之处。我之前使用 NavigationController.graph 定义了我的 AppBarConfig

// Old code
val controller by lazy { findNavController(R.id.nav_host_fragment) }
val appBarConfig by lazy { AppBarConfiguration(controller.graph) }

更新它以定义一组 top-level 个目的地纠正了在 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 中,而且我发现似乎对导航没有影响。

如果导航 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,类似于此代码:

yourElement.setOnClickListener {
  view.findNavController().navigate(R.id.nav_users)
}

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

我尝试通过代码修改startDestination。 它看起来运行良好,但在不保留 activity 的情况下,导航组件不会恢复片段堆栈。

我用虚拟 startDestination

解决了这个问题
  • startDestination 是 EmptyFragment(只是一个虚拟对象)
  • EmptyFragment 到 FirstFragment 操作需要 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不为空,片段自动恢复。