This is a new series of articles called “Modern Android Development Tips, “or “MAD Skills” for short. Stay tuned for this series of articles dedicated to helping developers build better modern Android development experiences.

Today is the third article in this series: Using SafeArgs when navigating your application. If you would like to review past posts, please refer to the link below:

  • Overview of navigation components
  • Navigate to the dialog box

This article focuses on SafeArgs, which is a navigation component and provides easier data transfer between different application destinations (interfaces).

Introduction to the

As you navigate to different destinations in your application, you may need to pass data. To avoid using global object references, better code encapsulation structures can be implemented through data passing, so that different fragments or activities only need to share the data they need.

Navigation components can transfer data through Bundles, and this mechanism can also be used to transfer data across Activities in Android.

We can do the same here, creating a Bundle for the data to be delivered and extracting it on the receiving side.

But there’s a better way to navigate components: SafeArgs.

SafeArgs is a Gradle plug-in that helps you enter data information that needs to be passed in a navigation diagram. It then generates code to help you navigate the tedious process of creating the Bundle, and extracts the data on the receiving side.

You can also use Bundle directly, but we recommend using SafeArgs. Not only is the code cleaner, but it adds type safety to the data, making the code more robust.

To show you how SafeArgs works, I will continue with the Donut Tracker application I demonstrated earlier at Dialog Destinations. If you want to follow along with this article, download the application source code and open it in Android Studio.

It’s time to make donuts

Here comes our Donut Tracking app again:

Donut Track: That’s the App. Here it goes again

The Donut Tracker displays a list of doughnuts, each with a name, description, and rating information, some of which I added or filled in by clicking the Hover Action button (FAB) in a dialog box that pops up.

Clicking the hover button will bring up a dialog box to fill in the new doughnut information

It’s not enough to be able to add information about new donuts, I also want to be able to modify information about existing donuts. Maybe I get a picture of a donut, or I want to raise my score.

The natural way to do this is to click on the list item and then open up the donut dialog where I can modify the donut information. But how does the app know which doughnut is displayed in the dialog? The code needs to pass information about the list item being clicked. Here, it needs to pass the id of the corresponding entry from the fragment where the list is located to the fragment where the dialog is located. Then the dialog can find the corresponding doughnut information from the database based on the ID and fill it into the form.

To pass the ID, here we use SafeArgs.

Using SafeArgs

Here I need to say that I have completed all the code, which is available on GitHubThe sample To find the complete code. So I’m going to walk you through each of these steps and show you what the code looks like, rather than just walking you through it.

First, I need to add some dependency libraries.

Unlike other modules of the navigation component, SafeArgs is not an API in itself, but a Gradle plug-in that generates code. So you need to set it up as a Gradle dependency and have it run correctly at build time to generate the required code.

First I added the following to the dependencies section of the project-level build.gradle file:

Def nav_version = "2.3.0" / / get the latest version number https://developer.android.google.cn/jetpack/androidx/releases/navigation classpath "Androidx. Navigation: navigation - safe - the args - gradle - plugin: $nav_version"Copy the code

Version 2.3.0 is used here. If you came across this article late, there should be an updated version for you to use. As long as it matches the versions of the other modules of the navigation component API that you are using.

I then added the following content to the build.gradle file of the app module. It makes it possible to generate the required code when SafeArgs is called.

apply plugin: "androidx.navigation.safeargs.kotlin"
Copy the code

Here, Gradle prompts you to Sync, so I click “Sync Now”.

This is a tip you shouldn’t ignore

Next, create and pass the required data in the navigation diagram.

The target interface for data is the donutEntryDialogFragment dialog box, which needs to know information about the object it wants to display. Clicking on the target screen will display the related properties on the right.

Clicking on the target screen displays a property list for that screen, where you can enter the data you want to pass

Click + in the Arguments pane to add data, bringing up the dialog shown below. Here I want to pass the doughnut information that I want to display, so I set the data type to Long, which is the same as the data type of the ID in the database.

This dialog box is displayed when you add data, and you can enter the data type, default values, and any other information you want

Note that when I set the data type to Long, the Nullable location grayed out. This is because in the Java programming language, the underlying data types (Integer, Boolean, Float, Long) are encapsulated based on primitive data types (int, bool, Float, Long) that cannot be null. Therefore, we need to ensure that the data is not null when using the underlying data type.

In addition it is important to note that the application now use the dialog box to add a new element (in the previous article I use the navigation module: dialog destination | MAD Skills has introduced), we also use the dialog box to edit existing elements. So the element ID is not necessarily passed, and when the user creates a new element, the code should be able to determine that there is no element information to display. So I typed -1 in the Default Value area of the dialog box because -1 is not a valid index Value. When the code navigates to the screen and no data is passed, -1 is passed as the default value, and the receiving code needs to use this value to determine that the user now needs to create a new donut.

At this point, we execute the Build operation and Gradle generates code for the input data. This is important because otherwise, Android Studio has no way of knowing where the function you want to call is in the auto-generated code.

You can find the result of executing the code generated in the above procedure under the “Java (Generated)” branch of the project structure tree. In the subdirectory, you can see that new files are generated, which are responsible for passing and retrieving data.

In DonutListDirections, you can find the companion object, which is the API for navigating to the dialog box.

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

Navigate () does not use the original Action, but instead uses the NavDirections object. It encapsulates both the action (through which we can navigate to the dialog) and the variables created earlier.

It is important to note the above actionDonutListToDonutEntryDialogFragment () function needs a Long arguments, before we create the relevant variables, and give it a value of 1. So if we call this function with no arguments, the method returns a NavDirections object with an itemId of -1.

In another DonutEntryDialogFragmentArgs generated file, you can see the fromBundle () function contains code to get the data from the target dialog:

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

Now I can successfully pass and retrieve data using the generated code. First, I write code in the DonutEntryDialogFragment class to get the itemId data and determine whether the user intends to add a new donut or edit an existing donut:

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

The first line of code uses a property delegate provided by the Navigation component library to simplify the process of retrieving data from the bundle. It allows you to find the name of the data directly in the ARGS variable.

If the user is editing information about an existing donut, the code here takes the information for that element 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
        }
    )
}
Copy the code

Note that the code here is requesting information from the database, and we want the entire request process to occur outside of the UI thread. So the code listens for the LiveData object provided by the ViewModel and asynchronously processes the request, populating the view when the data comes back.

When the user clicks the Done button in the dialog box, the information that the user enters needs to be stored. The following code updates the database and closes the dialog:

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

While the above code focuses on processing data in the target interface, let’s look at how to send data to the target interface.

There are two ways to go to the dialog box in the DonutList. One is when the user clicks the Hover operation button (FAB) :

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

Note that this code calls the no-argument constructor when the NavDirections object is created, so the variable will default to -1 (to indicate that this is a new donut), which is what we want to achieve by clicking the Hover action button.

Another approach is to open a dialog box when the user clicks on an existing element in the list. This can be done with the following lambda expression, which is passed in during the DonutListAdapter build (that is, the onEdit parameter) and then called when onClick is triggered for each entry:

donut ->
    findNavController().navigate(DonutListDirections
        .actionDonutListToDonutEntryDialogFragment(donut.id))
Copy the code

The code here is similar to the code when the user clicks the hover action button, except that it passes in the id of the entry, telling the dialog box that it wants to edit an existing element. And as we saw in our previous code, it populates the dialog with information about existing elements, and changes to this entry update the corresponding entries in the database accordingly.

conclusion

That’s all there is to SafeArgs. It is very simple to use (much simpler than bundles) because the dependency libraries help you generate code to simplify data passing and keep data type safe. In this way, you can take better advantage of data encapsulation, passing only the required data between destinations without exposing it on a larger scale.

Stay tuned for our next article on navigation components, which will show you how to use Deep Link.

For more information

For more details on navigation components, see the Getting Started documentation on navigation Components

For the complete code for the DonutTracker application, see the Github example

For more on the MAD Skills series, check out the Android Developers channel