Wassim Beltaief

Jetpack Navigation Component

Monday, August 19, 2019

The debate about single-activity vs multi-activity apps is old. Single-activity sounds clean but managing the back stack manually with Fragment transactions is a nightmare. Passing data between fragments, handling deep links, getting the back button right. All error-prone.

Navigation Component solves this.

The nav graph

You define all your destinations in an XML file:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.HomeFragment">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment">
        <argument
            android:name="itemId"
            app:argType="string" />
    </fragment>

</navigation>

All navigation lives here. You see the entire app flow in one file.

NavController

In the Activity, you set up the NavController:

val navController = findNavController(R.id.nav_host_fragment)
setupActionBarWithNavController(navController)

In a Fragment, navigating is one line:

findNavController().navigate(R.id.action_home_to_detail)

No more supportFragmentManager.beginTransaction().replace(...).addToBackStack(null).commit(). That was always fragile.

SafeArgs

Arguments between fragments used to be strings in a Bundle. Typos at runtime. SafeArgs generates typed classes from the nav graph:

// sending
val action = HomeFragmentDirections.actionHomeToDetail(itemId = "abc-123")
findNavController().navigate(action)

// receiving
val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId

If itemId is declared as string in the nav graph, args.itemId is a String. Compile-time safety for navigation arguments.

Deep links

Handling a deep link used to require parsing the intent URI manually in the Activity. With Navigation Component:

<fragment android:id="@+id/detailFragment" ...>
    <deepLink app:uri="example://detail/{itemId}" />
</fragment>

Add the <nav-graph> tag to the manifest and deep links work automatically. The back stack is created correctly too.

Bottom navigation

BottomNavigationView with Navigation Component handles tab switching and back stack correctly out of the box:

val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNav.setupWithNavController(navController)

Pressing back on a tab goes back within that tab's history, not to the previous tab. This is the correct behavior and it was annoying to implement manually before.

What I still manage manually

Shared element transitions between fragments are possible but require some boilerplate. Also, dialog destinations work but feel a bit clunky in the nav graph.

These are minor. Navigation Component makes the single-activity architecture practical and I now use it for every new project.