Skip to Content

Android Jetpack Navigation Pitfalls & Tricks

Table of content

  1. Send Args to fragments within activities
  2. Use startActivityForResult with the Navigation library
  3. Use generic naming for action names
  4. Reuse fragments in multiple navigation graphs
  5. TLDR
  6. Further readings

Navigation library from Jetpack offers a great variety of helpful tools, especially with the SafeArgs extension. My last project contained many different navigation flows paired with a large number of screens, so I decided to try the new navigation library which had just reached a stable release. With this article I would like to share some of the tricks and pitfalls I came across during development.

This article assumes that the reader already knows the library — if not, I recommend the official documentation as well as the linked articles in the TLDR section.

Send Args to fragments within activities

Sometimes it is necessary to start an activity which itself is a NavHost and pass arguments to the hosted fragment. E.g. imagine a ListFragment, which starts a DetailActivity with a DetailFragment. The DetailFragment needs an id of the item it displays.

With the classic Navigation Graph it would look like the following:

Click to expand!

<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/main_nav_graph"
    app:startDestination="@id/ListFragment">
    <fragment
        android:id="@+id/ListFragment"
        android:name="at.allaboutapps.ListFragment"
        android:label="fragment_list"
        tools:layout="@layout/fragment_list" />
    <activity
        android:id="@+id/DetailActivity"
        android:name="at.allaboutapps.DetailActivity"
        android:label="activity_detail"
        tools:layout="@layout/activity_detail">
        <argument
            android:name="id"
            app:argType="long" />
    </activity>
</navigation>
<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/detail_nav_graph"
    app:startDestination="@id/DetailFragment">
    <fragment
        android:id="@+id/DetailFragment"
        android:name="at.allaboutapps.DetailFragment"
        android:label="fragment_detail"
        tools:layout="@layout/fragment_detail" 
        <argument
            android:name="id"
            app:argType="long" />
    </fragment>
</navigation>

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/detailNavigationFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" 
        app:defaultNavHost="true"
        app:navGraph="@navigation/detail_nav_graph"/>
</FrameLayout>

The question now is: how do we pass the id argument to the DetailFragment? The library doesn’t offer us the option to define some kind of start argument for a navigation graph or similar. If we would deploy these two graphs with the default navigation attributes in the layout XML it would crash since the DetailFragment would not get the required arguments set and the library doesn’t know, where to retrieve these.

The trick is relatively simple:

  • Don’t set app:defaultNavHost and app:navGraph in the layout file
  • Set the NavHost programmatically
  • Set the arguments programmatically

The layout now looks as following:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/detailNavigationFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

And in the DetailActivity we need to add:

class DetailActivity : AppCompatActivity() {

  val args: DetailActivityArgs by navArgs()
  
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_detail)
    findNavController(R.id.detailNavigationFragment).setGraph(R.navigation.detail_nav_graph, args.toBundle())
  }
 }

As you can see, it doesn’t really cost us much effort to pass the arguments here. And since the arguments of DetailFragment and DetailActivity are called the same, the library can parse the fragment arguments and we have the id at the right place!

Use startActivityForResult with the Navigation library

Well, the simplest answer here is: you don’t. At the moment of writing this article, there is no equivalent for startActivityForResult. There are (some attempts to solve this, but in most of the cases it’s sufficient to use the default approach and still use startActivityForResult without the Navigation library.

But in case you don’t want to miss the type safety of the NavArgs and maybe extend the previous situation of passing arguments to the hosted fragment, it can look like this:

class DetailActivity : AppCompatActivity() {
  companion object {
    private const val EXTRA_ID = "id"
    private const val ACTIVITY_RESULT = "ACTIVITY_RESULT"
    fun getIntent(context: Context, id: Long): Intent =
      Intent(context, DetailActivity::class.java)
        .putExtra(EXTRA_ID, id)
  }
  
  lateinit var args: DetailActivityArgs
  
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_detail)
	args = DetailActivityArgs.fromBundle(intent.extras!!)
    findNavController(R.id.detailNavigationFragment).setGraph(R.navigation.detail_nav_graph, args.toBundle())
  }
  ...
  fun finishActivity(result: String) {
    val intent = Intent().putExtra(ACTIVITY_RESULT, result)
    setResult(Activity.RESULT_OK, intent)
    finish()
  }
 }

Use generic naming for action names

Let’s imagine the following situation: we have a registration of some sort with a lot of screens. In my case I had up to 12 fragments to handle:

Long registration

Awesome, right? Now, let’s say, we want to change the existing order of screens:

  • Without the Navigation library it’s a mess, every fragment has a reference to the next one and we don’t have any overview which is linked to which at the moment.
  • With the Navigation Editor of Android Studio we have at least an overview which fragment is currently linked to which. But still, if we change something in the graph, we need to change the fragments itself as well. Not really handy here.

The solution is pretty simple: use generic action names. Don’t use names like goToFirstName(), custom to each fragment, but use names like next(), which is used everywhere the same. The corresponding navigation graph to the image looks as following:

Click to expand!

<?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"
    android:id="@+id/registration_nav_graph"
    app:startDestination="@id/RegisterNameFragment">

    <fragment
        android:id="@+id/RegisterBirthFragment"
        android:name="at.allaboutapps.RegisterBirthFragment"
        android:label="fragment_register_birth" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterStateFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterCompanionFragment"
        android:name="at.allaboutapps.RegisterCompanionFragment"
        android:label="fragment_register_companion" />

    <fragment
        android:id="@+id/RegisterContactFragment"
        android:name="at.allaboutapps.RegisterContactFragment"
        android:label="fragment_register_contact" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterCompanionFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterDestinationFragment"
        android:name="at.allaboutapps.RegisterDestinationFragment"
        android:label="fragment_register_destination" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterTimeFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterEmailFragment"
        android:name="at.allaboutapps.RegisterEmailFragment"
        android:label="fragment_register_destination" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterTelephoneFragment" />
    </fragment>

    <fragment
        android:id="@+id/RegisterGenderFragment"
        android:name="at.allaboutapps.RegisterGenderFragment"
        android:label="fragment_register_gender" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterBirthFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterPassportFragment"
        android:name="at.allaboutapps.RegisterPassportFragment"
        android:label="fragment_register_passport" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterEmailFragment" />
    </fragment>

    <fragment
        android:id="@+id/RegisterNameFragment"
        android:name="at.allaboutapps.RegisterNameFragment"
        android:label="fragment_register_name" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterGenderFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterRouteFragment"
        android:name="at.allaboutapps.RegisterRouteFragment"
        android:label="fragment_register_route" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterContactFragment"/>
    </fragment>

    <fragment
        android:id="@+id/RegisterStateFragment"
        android:name="at.allaboutapps.RegisterStateFragment"
        android:label="fragment_register_state" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterPassportFragment" />
    </fragment>

    <fragment
        android:id="@+id/RegisterTelephoneFragment"
        android:name="at.allaboutapps.RegisterTelephoneFragment"
        android:label="fragment_register_telephone" >
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterDestinationFragment" />
    </fragment>

    <fragment
        android:id="@+id/RegisterTimeFragment"
        android:name="at.allaboutapps.RegisterTimeFragment"
        android:label="fragment_register_time">
        <action
            android:id="@+id/next"
            app:destination="@id/RegisterRouteFragment"/>
    </fragment>

</navigation>

With this setup we can easily change the order of the screens by simply changing the NavGraph — the library generates us the Directions and we don’t need to touch the fragments!

Reuse fragments in multiple navigation graphs

There might be some circumstances where it makes sense to reuse the same fragment in different NavGraphs. E.g. if a registration uses in some sub-flows the same fragments.

First: try to avoid this situation. The Navigation library is meant to be used within a single-activity application and doesn’t really like the reuse of the same fragments in different NavGraphs. The ugly thing here is, that each NavGraph generates actions and arguments with the same name. And if two NavGraphs using the same fragment with different actions and arguments, they are overwriting each other in a non-deterministically way.

There are some tricks to work around this:

  • Avoid this situation

    Seriously, this is nasty. Since the NavGraph processing happens non-deterministically it can happen that the local build is fine but fails on the build server because it’s overridden in a different way.

  • Use fake arguments

    If one NavGraph uses more arguments for a fragment than another one, still include these arguments into both graphs and think about reasonable default values. Don’t rely on it, that the NavGraph with more arguments gets processed first — in doubt that’s not the case.

  • Use navigate() with bundle instead of actions in edge cases

    In edge cases e.g. if a fragment is used at the end of a flow (meaning no further action) in one NavGraph and in the middle of the flow (meaning with further actions) don’t (conditionally) call findNavController().navigate(FragmentDirections.goToDetailFragment(id)) but instead use findNavController().navigate(R.id.DetailFragment, bundleOf("id" to 5)). This way you don’t rely on the generated direction but can still use the Navigation and SafeArgs features of the DetailFragment.

TLDR

  • Args can easily be passed from activity to fragment by setting the NavHost programmatically
  • There is no equivalent of startActivityForResult, but we can generate the Args from the intent extras.
  • Generic naming of actions helps to easily change the order of screens within a flow.
  • Try to avoid using the same fragment with different arguments & actions in multiple NavGraphs since the code might break due to nondeterministic class generation.

Further readings