Jetpack Navigation Component
Monday, August 19, 2019The 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.