95

Basically, I have the following navigation graph:

enter image description here

I want to change my starting point in navigation graph to fragment 2 right after reaching it (in order to prevent going back to fragment 1 when pressing back button - like with the splash screen).

This is my code:

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

But, obviously it's not working and it gets back to fragment 1 after pressing back button.

Am I doing it wrong? Is there any other solution?

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
Shynline
  • 1,497
  • 1
  • 13
  • 18
  • What do you want to achieve after pressing back button on the fragment 2? – Denysole Jul 04 '18 at 12:22
  • i want to exit the app in that case – Shynline Jul 04 '18 at 13:24
  • 2
    It is possible to [change starting point programmatically in navigation graph](https://stackoverflow.com/questions/51929290/is-it-possible-to-set-startdestination-conditionally-using-android-navigation-ar). see [here](https://stackoverflow.com/questions/51929290/is-it-possible-to-set-startdestination-conditionally-using-android-navigation-ar) to get [solution](https://stackoverflow.com/questions/51929290/is-it-possible-to-set-startdestination-conditionally-using-android-navigation-ar). – Akash Patel Aug 30 '18 at 05:21
  • 1
    I didn't even know you could set the graph programmatically, thank you this question has helped me alot! – dave o grady Mar 03 '20 at 21:54
  • 1
    Not sure why your code is not working. I'm setting it in the onCreate and no problem. Your code in your question solved my issue :) thnx! – Boy Feb 19 '21 at 06:33
  • The code in the question works great in the setup phase, i.e. in the onCreate() method of the Activity, which contains the NavHostFragment. Before, I assumed setStartDestination() would be sufficient because it operates on the original NavGraph instance. But apparently only calling setGraph() propagates the change. – Peter F Mar 05 '21 at 08:58

8 Answers8

94

UPDATE:

When you have nav graph like this:

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

And you want to navigate to the second fragment and make it root of your graph, specify the next NavOptions:

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

And use them for the navigation:

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.


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

And then on your code:

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

Old answer

Deprecated: The clearTask attribute for actions and the associated API in NavOptions has been deprecated.

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


If you want to change your root fragment to fragment 2 (e.g. after pressing back button on fragment 2 you will exit the app), you should put the next attribute to your action or destination:

app:clearTask="true"

Practically it looks in a next way:

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

I've added app:clearTask="true" to action.


Now when you perform navigation from fragment 1 to fragment 2 use the next code:

Navigation.findNavController(view)
        .navigate(R.id.action_firstFragment_to_secondFragment);
desgraci
  • 1,191
  • 1
  • 11
  • 25
Denysole
  • 3,903
  • 1
  • 20
  • 28
  • 2
    clearTask flag and setClearTask are deprecated since alpha02, check out these https://stackoverflow.com/questions/50514758/how-to-clear-navigation-stack-after-navigating-to-another-fragment-in-android/50741496, https://stackoverflow.com/questions/50336112/how-to-disable-up-in-navigation-for-some-fragment-with-the-new-navigation-archit – theme_an Aug 31 '18 at 08:04
  • This is NOT working for me. Where are you putting this code? Like you I have a Welcome fragment that is set as the home fragment. In the welcome fragment's onActivityCreated method I get my viewmodel, check to see if the user is logged in, and if so I tried your solution from above. I still have a up arrow on 2nd (new home) fragment. How can I get rid of this? – szaske Oct 26 '18 at 18:03
  • @szaske please, create another question and provide your source code, so I'll be able to help you – Denysole Oct 27 '18 at 12:03
  • I'm confused. I submitted my solution below with my code. I understand you answered your own origin question, but I had the same issue as you and your solution only partially solved my problem. Is it considered bad manners to suggest another approach? – szaske Oct 27 '18 at 23:16
  • This works, upvoted, but I have found that it actually breaks the back functionality, leaving an empty navHostFragment on the activity if for example your firstFragment was used on the navigation start, is there an elegant way to close the container activity using this? – desgraci May 17 '19 at 11:44
  • @desgraci are you using `setPopUpTo()` with `true` as a second argument? – Denysole May 17 '19 at 11:46
  • @AlexeyDenysenko yes sir! `firstFragment` is my `startDestination` I do the navigation to the `secondFragment` like proposed and when I press back on the `secondFragment` the activity doesn't finish, no special configuration to reproduce it, and the code is kept to minimum to avoid other flags affecting the nav – desgraci May 17 '19 at 11:49
  • @desgraci ok, I'll check current behavior and reach you out with some answer – Denysole May 17 '19 at 11:51
  • @AlexeyDenysenko Thanks in advance! – desgraci May 17 '19 at 11:53
  • 1
    be aware people that if you would attempt to do a findNavController.popBackstack from Fragment2 in an attempt to go to Fragment1 , it wont work this way if you have implemented the solution above. The above solution is only going to work for the manual backpress. popBackStack will return false and you would be stuck on Fragment2. – Muhammad Ahmed AbuTalib Jul 20 '19 at 15:11
  • @MuhammadAhmedAbuTalib inded you are right. At this scenario how did you fix this behavior? – Archie G. Quiñones Sep 26 '19 at 07:53
47

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

Remove startDestination from nav_graph.xml

?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>
Dino Sunny
  • 921
  • 1
  • 10
  • 18
21

I found a solution for this, but it's ugly. I guess this it to be expected with an alpha library, but I hope Google looks into simplifying/fixing this as this is a pretty popular navigation pattern.

Alexey's solution did not work for me. My problem was that I have up arrows showing on my Actionbar by using:

NavigationUI.setupActionBarWithNavController(this, navController)

If I did as Alexey suggests above, my new start fragment still had a arrow pointing to my initial start fragment. If I pressed that up arrow my app would sort-of restart, transitioning to itself (the new start fragment)

Here is the code needed to get to what I wanted which was:

  • Fragment #1 is where my application initially starts
  • I can do an Auth check in Fragment #1 and then programmatically change the start to fragment #2.
  • Once in Fragment #2 there is no up arrow and pressing the back button does not take you to Fragment #1.

Here is the code that accomplishes this. In my Activity's 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)

and also:

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

Then in Fragment #1's onActivityCreated, per Alexey's suggestion:

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

The key code is: (activity as MainActivity).makeHomeStart() which just runs a method in the activity that changes the graphs startDestination. I could clean this up and turn it into an interface, but I'll wait for Google and hope they improve this whole process. The method 'setPopUpTo' seems poorly named to me and it's not intuitive that your naming the fragment that is getting cut out of the graph. It's also strange to me that they're making these changes in navOptions. I would think navOptions would only relate to the navigation action they're connected to.

And I don't even know what navHost.navController.graph = graph does, but without it the up arrows return. :(

I'm using Navigation 1.0.0-alpha06.

szaske
  • 1,887
  • 22
  • 32
  • I'm not sure why you're getting down voted as this is quite a useful answer and it does handle the OP's problem. The weird graph assignment needs to be done since you are inflating the navigation fragment directly and not through the usual "findNavController" machinery. I've updated your post with a variation as opposed to posting an alternative answer. – Carel Apr 06 '19 at 22:57
  • Thank you for your answer. Is there any better solution now? – Francis Feb 15 '20 at 11:13
15

You can also try the followings.

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

Instead of trying to pop start destination or navigate manually to target destination, it would be better to have another navigation graph with different workflow. This would be even better for the case when you want completely different navigation flow conditionally.

Doctiger
  • 2,318
  • 2
  • 15
  • 28
  • have any idea about this demo i made it but dont know what is the wright proceducer this with bottomnavigationview and viewpager https://github.com/sunil-singh-chaudhary/Jetpac-Navigation-Activity-with-Fragments – Sunil Chaudhary Aug 20 '20 at 06:17
  • @SunilChaudhary Please refer this link it includes the guide about how to update the UI. https://developer.android.com/guide/navigation/navigation-ui, but in your case, setting up viewpager with BottomNavigationView is not something for navigation component I think. – Doctiger Aug 20 '20 at 07:58
4

You don't really need to pop the Splash Fragment. It can remain there for the rest of your App life. What you should do is from the Splash Screen determine which next Screen to Show.

Login State Machine flow

In the picture above you can ask in the Splash Screen State if there is a saved LoginToken. In case is empty then you navigate to the Login Screen.

Once the Login Screen is done, then you analyze the result save the Token and navigate to your Next Fragment Home Screen.

When the Back Button is Pressed in the Home Screen, you will send back a Result message to the Splash Screen that indicates it to finish the App.

Bellow code may help:

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)
Pablo Valdes
  • 734
  • 8
  • 19
3

For those who have a navigation xml file with similar content to this:

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

suppose current fragment opened is the home fragment and you want to navigate to users fragment, for that just call in the setOnClickListener of the element that you want to navigate to the navigate method from the nav controller similar to this code:

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

that will make the app navigate to that other fragment and will also handle the title in the toolbar.

Juan Ricardo
  • 143
  • 8
2

Okay, after messing with this for a bit I found a solution that worked for me that didn't require a ton of work.

It appears two things MUST be in place for it function as if your secondFragment is your start destination.

use the ALTERNATIVE option in the accepted post

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

The above will remove firstFragment from the stack and inflate secondFragment when moving. The app cannot step back to firstFragment anymore BUT your left with secondFragment showing a back arrow as @szaske stated.

This is what made the difference. I previously defined my AppBarConfig using the NavigationController.graph like so

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

Updating it to define a set of top-level destinations rectified the issue of showing the back arrow on secondFragment instead of a hamburger menu icon.

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

Setting the start destination may or may not have negative implications in your project so do it as needed however in this example we do not need to do so. If it makes you warm and fuzzy to ensure that your graph has the correct start fragment defined, you can do it like so.

controller.graph.startDestination = R.id.secondFragment

Note: Setting this does not prevent the back arrow from occurring in secondFragment and from what I have found seems to have no effect on navigation.

DevinM
  • 1,112
  • 1
  • 12
  • 29
  • When/where do you use controller and appBarConfig? – pokumars Apr 23 '21 at 19:21
  • @pokumars You would do this in your activity when you are setting the action bar setupActionBarWithNavController(navController, appBarConfig) – DevinM May 04 '21 at 14:21
1

I tried to modify code in startDestination. It works well, but It does not keep the activity, the Navigation component does not restore fragment stack.

I resolved this problem with a dummy startDestination

  • startDestination is EmptyFragment(just a dummy)
  • EmptyFragment to FirstFragment action require popUpTo=EmptyFragment and popUpToInclusive=true

NavGraph image

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

when Activity is recreated, savedInstanceState is not null and fragment is restored automatically.

RandomType
  • 47
  • 1
  • 8
tijins
  • 21
  • 3