Navigating with SafeArgs

I don’t want to have an argument. But if I do, I’ll use SafeArgs.

Chet Haase
Android Developers
Published in
9 min readOct 19, 2020

--

This is the third in a series of articles about the Navigation component API and tool. These articles are based on content that is also explained in video form, as part of the MAD Skills series, so feel free to consume this material in whichever way you prefer (though the code tends to be easier to copy from text than from a video, which is why we offer this version as well).

Here’s the video version:

This episode is on SafeArgs, the facility provided by Navigation component for easily passing data between destinations.

Introduction

When navigating to different destinations in your application, you may want to pass data. Passing data, as opposed to using references to global objects, allows for better encapsulation in your code so that different fragments or activities only need to share the pieces that directly concern them.

The Navigation component enables passing arguments with Bundles, which is the general mechanism used in Android for passing information between different activities.

And we could totally do that here, creating a Bundle for the arguments we want to pass and then pulling them out of the Bundle on the other side.

But Navigation has something even better: SafeArgs.

SafeArgs is a gradle plugin that allows you to enter information into the navigation graph about the arguments that you want to pass. It then generates code for you that handles the tedious bits of creating aBundle for those arguments and pulling those arguments out of the Bundle on the other side.

You could totally use raw Bundles directly… but we recommend using SafeArgs instead. It’s not only easier to write — with a lot less code for you to maintain — but it also enables type-safety for your arguments, making your code inherently more robust.

To show how SafeArgs works, I’ll keep working with the Donut Tracker app that I demo’d in the previous episode, Dialog Destinations. If you want to follow along at home, download the app and load it into Android Studio.

Time to Make the Donuts

Here’s our donut tracking app again:

Donut Tracker: the app!

Donut Tracker shows a list of donuts, each of which has name, description, and rating information that I added or in a dialog accessed by clicking on the floating action button:

Clicking on the FAB brings up a dialog to enter information about a new donut

It’s not good enough to be able to add information for new donuts; I also want to be able to change information about existing donuts. Maybe I got a picture of one in the wild, or I want to upgrade my rating for that donut — who knows?!?!

A natural way to do this would be to click on one of the items in the list, which would take me to the same dialog destination as before, where I could update the information about that item. But how does the app know which item to present in the dialog? The code needs to pass information about the item that was clicked. In particular, it needs to pass the item’s underlying id from the list fragment to the dialog fragment, then the dialog can retrieve information from the database for the donut with that id and can then populate the form appropriately.

To pass that id, I’ll use SafeArgs.

Using SafeArgs

At this point, I should point out that I already wrote all of this code, and that’s what you will find in the sample on GitHub; the finished code. So rather than take you through the steps as I do them, I will explain what I did, and you can see the results in the sample code.

First of all, I needed some library dependencies.

SafeArgs is not the same kind of library module as the other parts of navigation; it’s not an API per se, but rather a gradle plugin that will generate code. So I needed to pull it in as a gradle dependency and then apply the plugin to run at build time, to generate the necessary code.

I first added this to the dependencies block of the project’s build.gradle file:

def nav_version = "2.3.0"
classpath “androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version”

I used version 2.3.0, but if you’re reading this much later, there’s probably a newer version which you can use instead. Just use whatever version you are using for the other parts of the Navigation component API.

I then added the following command to the app’s build.gradle file. This is the piece that causes the code to be generated for the SafeArgs calls.

apply plugin: "androidx.navigation.safeargs.kotlin"

At this point, gradle complained that it wanted to sync, so I clicked “Sync Now.”

This is a nag to sync that you shouldn’t ignore

Next, I went to the navigation graph, to create and pass the necessary data.

The destination that needed an argument is the dialog, donutEntryDialogFragment, which needed information about which item to display. Clicking on that destination showed the destination properties over to the right:

Clicking on a destination brings up the property sheet for that destination, which is where you can enter arguments to pass to it

I clicked on the + sign in the Arguments section to add a new argument, which brought up the dialog below. I wanted to pass information about which donut item to display, so I chose Long as the type, to correspond with the type of the id in the database.

Adding an Argument takes you to this dialog where enter the type, default value (if any) and other information as needed

Note that the Nullable item was grayed out when I chose Long. This is because the base types allowed (Integer, Boolean, Float, and Long) are backed by primitive types (int, bool, float, and long) at the Java programming language layer, and these types cannot be null. So even though Kotlin’s Long type is nullable, the underlying primitive long type is not, so we are constrained to non-nullable types when using these base types.

Another thing to note is that the app now uses the dialog destination both for entering a new item (which I covered in the last episode) and for editing an existing item. There won’t always be an itemId to pass along; when the user creates a new item, the code should indicate that there is no existing item to display. That’s why I entered -1 for a Default Value in the dialog, to indicate that situation, since -1 is not a valid index. When the code navigates to this destination with no argument supplied, the default value of -1 will be sent and the receiving code will use that value to decide that a new donut is being created.

At this point, I ran a build, which caused gradle to generate the code for the information I had entered. This was important because otherwise Studio wouldn’t know about the generated functions that I wanted to call, and isn’t auto-complete just grand? (Note: This step is no longer needed in the latest version of Android Studio 4.2; I just tried it on canary 15 and it was able to auto-complete without having to build and generate the code first.)

You can see the results of the generated code by going to the “java (generated)” files in the project list. Inside one of the subfolders, you can see the new files that were generated to pass and retrieve the argument.

In DonutListDirections, you can see the companion object, which is the API I use to navigate to the dialog.

companion object {
fun actionDonutListToDonutEntryDialogFragment(
itemId: Long = -1L): NavDirections =
ActionDonutListToDonutEntryDialogFragment(itemId)
}

Instead of using an Action, which the navigate() call originally used, the code navigate()s using the NavDirections object, which encapsulates both the action (which takes us to the dialog destination) and the argument created earlier.

Note that the actionDonutListToDonutEntryDialogFragment() function above takes a Long parameter, which is the argument we created, and supplies it with a default value of -1. So if we call the function with no argument, it will return a NavDirections object with an itemId parameter of -1.

In the other generated file, DonutEntryDialogFragmentArgs, you can see the fromBundle() code generated that can be used to retrieve the argument on the other side, in the dialog destination:

fun fromBundle(bundle: Bundle): DonutEntryDialogFragmentArgs {
// ...
return DonutEntryDialogFragmentArgs(__itemId)
}

Now I could use these generated functions to successfully pass and retrieve the data. First, I created the code in the DonutEntryDialogFragment class to get the itemId argument and decide whether the user is adding a new donut or editing an existing one:

val args: DonutEntryDialogFragmentArgs by navArgs()
val editingState =
if (args.itemId > 0) EditingState.EXISTING_DONUT
else EditingState.NEW_DONUT

That first line of code uses a property delegate supplied by the Navigation component library which makes retrieving the argument from a bundle easier. It allows us to refer directly to the name of the argument inside of the args variable.

If the user is editing an existing donut, the code retrieves that item’s information and populates the UI with it:

if (editingState == EditingState.EXISTING_DONUT) {
donutEntryViewModel.get(args.itemId).observe(
viewLifecycleOwner,
Observer { donutItem ->
binding.name.setText(donutItem.name)
binding.description.setText(donutItem.description)
binding.ratingBar.rating = donutItem.rating.toFloat()
donut = donutItem
}
)
}

Note that this code queries the database for information, and we want that to happen off of the UI thread. Thus the code observes the LiveData object provided by the ViewModel and handles the request asynchronously, populating the views whenever that data comes in.

When the user clicks the Done button in the dialog, it’s time to save the information they entered. The code updates the database with the data in the dialog’s UI and then dismisses the dialog:

binding.doneButton.setOnClickListener {
donutEntryViewModel.addData(
donut?.id ?: 0,
binding.name.text.toString(),
binding.description.text.toString(),
binding.ratingBar.rating.toInt()
)
dismiss()
}

The code we just walked through handles the argument on the destination side; now let’s take a look at how the data gets sent to that destination.

There are two places, in DonutList, which navigate to the dialog. One handles the situation when the user clicks on the FloatingActionButton (FAB):

binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment())
}

Note that the code creates the NavDirections object with no constructor argument, so the argument will get a default value of -1 (indicating a new donut) which is what we want from a click on the FAB.

The other way to navigate to the dialog happens when the user clicks on one of the existing items in the list. This action ends up in this lambda, which is passed into the creation of the DonutListAdapter code (as the parameter onEdit) and called in the onClick handler for each item:

donut ->
findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment(donut.id))

This code is similar to the code above called when the user clicks on the FAB, but in this case it uses the id of the item clicked on, to tell the dialog that it will be editing an existing item. And as we saw in the earlier code, that causes the dialog to be populated with the values from that existing item, and changes to that data will update that item in the database.

Summary

That’s it for SafeArgs. They are simple to use (much simpler than playing around with Bundles!) because the library generates code for you to simplify passing data in a type-safe and easy manner between destinations. This allows you to take advantage of data encapsulation by passing only the data you need between destinations instead of exposing it more broadly.

Stay tuned for one more episode about Navigation component, where I will explore how to use Deep Links.

For More Information

For more details on Navigation component, check out the guide Get started with the Navigation component on developer.android.com.

To see the finished Donut Tracker app (which contains the code outlined above, but also the code covered in future episodes), check out the GitHub sample.

Finally, to see other content in the MAD Skills series, check out the video playlist in the Android Developers channel on YouTube.

--

--

Chet Haase
Android Developers

Android and comedy. Not necessarily in that order.