95

I am using Navigation from Android Jetpack to navigate between screens. Now I want to set startDestination dynamically.

I have an Activity named MainActivity And two Fragments, FragmentA & FragmentB.

var isAllSetUp : Boolean = // It is dynamic and I’m getting this from Preferences.

    If(isAllSetUp)
    {
     // show FragmentA
    }
    else
    {
     //show FragmentB
    }

I want to set above flow using Navigation Architecture Component. Currently I have used startDestionation as below but it’s not fulfilling my requirement.

<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/lrf_navigation"
   app:startDestination="@id/fragmentA">

   <fragment
       android:id="@+id/fragmentA"
       android:name="com.mindinventory.FragmentA"
       android:label="fragment_a"
       tools:layout="@layout/fragment_a" />
</navigation>

Is it possible to set startDestination conditionally using Android Navigation Architecture Component?

Akash Patel
  • 3,091
  • 3
  • 15
  • 23
  • 1
    #navigation-architecture-component #navigation-graph – Akash Patel Aug 20 '18 at 11:00
  • check this https://stackoverflow.com/questions/51173002/how-to-change-start-destination-of-a-navigation-graph-programmatically-jet-pac – AskNilesh Aug 20 '18 at 11:01
  • Hi @NileshRathod, It's work if i open FragmentA first then FragmentB. but i want to set StartDestination programmatically. Do you have any solution for same? – Akash Patel Aug 20 '18 at 11:16
  • Try this `NavigationUI.onNavDestinationSelected(menuItem, navController.getNavController());` – AskNilesh Aug 20 '18 at 11:26
  • 1
    I want to do above thing without menu or any other navigation control. maybe there is no way to set startDestination conditionally. so for now I used [https://stackoverflow.com/questions/51173002/how-to-change-start-destination-of-a-navigation-graph-programmatically-jet-pac] as per you suggested. – Akash Patel Aug 21 '18 at 05:33
  • A dynamic extension added in this answer : [Change destination dynamically](https://stackoverflow.com/a/62968347/3248593) – Morteza Rastgoo Jul 18 '20 at 12:08

5 Answers5

125

Finally, I got a solution to my query...

Put below code in onCreate() method of Activity.

Kotlin code

val navHostFragment = (supportFragmentManager.findFragmentById(R.id.home_nav_fragment) as NavHostFragment)
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.nav_main)
//graph.addArgument("argument", NavArgument)
graph.setStartDestination(R.id.fragment1)
//or
//graph.setStartDestination(R.id.fragment2)

navHostFragment.navController.graph = graph

Java code

NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.home_nav_fragment);  // Hostfragment
NavInflater inflater = navHostFragment.getNavController().getNavInflater();
NavGraph graph = inflater.inflate(R.navigation.nav_main);
//graph.addArgument("argument", NavArgument)
graph.setStartDestination(R.id.fragment1);

navHostFragment.getNavController().setGraph(graph);
navHostFragment.getNavController().getGraph().setDefaultArguments(getIntent().getExtras());


NavigationView navigationView = findViewById(R.id.navigationView);
NavigationUI.setupWithNavController(navigationView, navHostFragment.getNavController());

Additional Info

As @artnest suggested, remove the app:navGraph attribute from the layout. It would look something like this after removal

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/home_nav_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

In the case of a fragment tag used instead of FragmentContainerView, the above changes remain the same

Boken
  • 4,825
  • 10
  • 32
  • 42
Akash Patel
  • 3,091
  • 3
  • 15
  • 23
  • 19
    First, you need to remove app:navGraph NavHostFragment attribute from your Activity layout xml as NavHostFragment instantiates and sets the navigation graph after host Activity onCreate() call. Also if you use NavigationUI.setupWithNavController method, for example for Toolbar or BottomNavigationView, you need to put this code snippet above NavigationUI.setupWithNavController method call. – theme_an Aug 29 '18 at 12:12
  • 2
    I'd also add that to preserve back button behavior in the Kotlin code, you still need a way to set the `navHostFragment` as the default nav host. This can be accomplished with this transaction: `supportFragmentManager.beginTransaction().setPrimaryNavigationFragment(navHostFragment).commit()` – Chris Oct 30 '18 at 15:47
  • 1
    the method graph.setDefaultArguments() doesn't exist anymore, you also don't need the last 2 lines (NavigationUI). See [my answer](https://stackoverflow.com/a/53992737/852336) for an updated version. – Danish Khan Jan 01 '19 at 03:05
  • Is it possible to add transition animations while settings the graph? – Maddy Mar 25 '20 at 07:25
  • 1
    Will deep linking work since it is adding startDestination when you jump to deep destination? What will it add with this setup? – uberchilly Aug 24 '20 at 19:31
59

Some of the APIs have changed, are unavailable or are not necessary since Akash's answer. It's a bit simpler now.

MainActivity.java:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  NavHostFragment navHost = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_main_nav_host);
  NavController navController = navHost.getNavController();

  NavInflater navInflater = navController.getNavInflater();
  NavGraph graph = navInflater.inflate(R.navigation.navigation_main);

  if (false) {
    graph.setStartDestination(R.id.oneFragment);
  } else {
    graph.setStartDestination(R.id.twoFragment);
  }

  navController.setGraph(graph);
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
  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:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <!-- Following line omitted inside <fragment> -->
  <!-- app:navGraph="@navigation/navigation_main" -->
  <fragment
    android:id="@+id/fragment_main_nav_host"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="androidx.navigation.fragment.NavHostFragment"
  />

</androidx.constraintlayout.widget.ConstraintLayout>

navigation_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- Following line omitted inside <navigation>-->
<!-- 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/oneFragment"
    android:name="com.apponymous.apponymous.OneFragment"
    android:label="fragment_one"
    tools:layout="@layout/fragment_one"/>
  <fragment
    android:id="@+id/twoFragment"
    android:name="com.apponymous.apponymous.TwoFragment"
    android:label="fragment_two"
    tools:layout="@layout/fragment_two"/>
</navigation>
Danish Khan
  • 1,514
  • 3
  • 15
  • 22
  • 1
    What if this startDestination needs a bundle ? – Damien Locque Jan 30 '19 at 14:14
  • How would I go, then, from `twoFragment` back to `oneFragment`, removing `twoFragment` from the back stack? I mean, the Navigation API equivalent of `getSupportFragmentManager().beginTransaction().replace(R.id.nav_host_fragment, new OneFragment()).commit();`? Also, could you show where to put `setSupportActionBar()` and `NavigationUI.setupWithNavController()`? Thanks – Oliver Feb 05 '19 at 11:15
  • 3
    @DamienLocque @Manikanta use `navController.setGraph(graph, bundle)` – amitavk Apr 12 '19 at 11:08
  • use navController.setGraph(graph, intent.extras) – Dino Sunny Jul 17 '20 at 05:58
13

This can be done with navigation action. Because fragmentA is your start destination, so define an action in fragmentA.

Note these two fields: app:popUpToInclusive="true" app:popUpTo="@id/fragmentA"

<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/lrf_navigation"
   app:startDestination="@id/fragmentA">

   <fragment
       android:id="@+id/fragmentA"
       android:name="com.mindinventory.FragmentA"
       android:label="fragment_a"
       tools:layout="@layout/fragment_a">

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

   <fragment
       android:id="@+id/fragmentB"
       android:name="com.mindinventory.FragmentB"
       android:label="fragment_b"
       tools:layout="@layout/fragment_b"/>
</navigation>

When your MainActivity started, just do the navigation with action id, it will remove fragmentA in the stack, and jump to fragmentB. Seemingly, fragmentB is your start destination.

if(!isAllSetUp)
{
  // FragmentB
  navController.navigate(R.id.action_a_to_b)
}
LeeR
  • 568
  • 6
  • 10
  • Thank´s, nice clean solution! You can also appearently do this in onCreate of the fragment. I´m doing it there, because I also want it to happen also when the fragment is called from the drawer menu in my case. – Max Jan 28 '20 at 17:38
  • Though navigate() dos not kill the fragment immidiately (even if it´s removed from the backstack in the process). So, my application actually crashed in onViewCreated. I fixed it, but it seems like a bit of an ugly hack to early return out of onViewCreated, when it navigated away. – Max Jan 29 '20 at 16:18
3

You can set your starting destination programmatically, however, most of the time your starting logic will consult some remote endpoint. If you don't show anything on screen your UI will look bad.

What I do is always show a Splash screen. It will determine which is the next Screen to Show.

enter image description here

For instance, in the picture above you can ask in the Splash Screen State if there is a saved LoginToken. In case it's 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.

To pop 1 Fragment back and navigate to another you can use the following code:

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
  • If you deep link from notification your backstack will contain splash screen as well – uberchilly Aug 24 '20 at 21:44
  • At first I had my splash screen contain NavController instructions, but the issue comes when using asynch callbacks from reactive components..., at times, they do not reach the splash screen on time before the splash screen already took a decision. I've found that async direction handlers are best tied to the main activity lifeCycle since it will survive any change between splash-screen(AKA home) and any of its sub directions. There is ofc the option of clearing the cache of the publisher before reconnecting, but this is already too intrusive imo. – Delark Sep 24 '21 at 17:31
3

this is not an answer but Just a replication of @Akash Patel answer in more clean and clear way

// in your MainActivity
navController = findNavController(R.id.nav_host_fragment)
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
if (Authentication.checkUserLoggedIn()) {
      graph.startDestination = R.id.homeFragment
} else {
      graph.startDestination = R.id.loginFragment
}
navController.graph = graph
Bahaa KA
  • 157
  • 1
  • 12
  • 1
    If you use this solution, you will get a crash if activities no longer exists `java.lang.IllegalStateException: unknown destination during restore: com.x:id/navigation_on_boarding` – rafaelasguerra Dec 05 '19 at 12:52
  • did you tried it ? i already use it and it has no side effects at all could you please explain more about what you already tried and lead to having `illegalStateException` so i can check it on my setup and get back to you with feedback – Bahaa KA Dec 05 '19 at 15:05