[MUSIC PLAYING]
CHET HAASE: Hi.
Welcome to this third episode in the navigation series.
Today, we're going to talk about SafeArgs.
When you navigate between different destinations
in your application, you may want to pass some arguments.
Passing arguments is good because it
means that you don't have to have some global declared
in your application just so that different destinations can have
shared access to this thing.
It's better for data encapsulation
if you just pass the information that that new destination
needs.
Navigation component provides a mechanism
for doing this using bundles, which is the general Android
mechanism for passing information
between things like activities.
And you could totally use bundles here.
On one side, you could stuff the arguments into a bundle,
and then on the other side, you could extract the arguments out
from the bundle.
But Navigation component provides something better,
called SafeArgs.
SafeArgs is a Gradle plug-in that
allows you to enter information about the arguments you
want to pass in the navigation graph.
Then it generates code for you to deal
with all the tedious bits of bundles,
putting the information in, taking the information out
on the other side.
Not only that, but it also provides type safety
because it knows what types of information to provide,
to put into the bundle and to take out of the bundle.
So we recommend using SafeArgs when you are passing arguments
with a Navigation component.
So to demonstrate this, we're going
to take another look at the application we've
been looking at in previous episodes, the Donut Tracker.
So here's our donut tracking app.
It shows a list of donuts, each of which
has a name, an optional description, and rating
information that I added in a form in a dialog.
So we can add another donut.
We click on the fab.
We add this information there.
We add a description.
We add a rating.
And then it adds that to the list.
But ideally, we would also be able to edit information
about these items.
So let's say I wanted to change the description
for a new donut.
So I should be able to click on that.
And that doesn't work, because I haven't plugged in that code
yet.
So what do we need to be able to do that?
So we need to pass information to the dialog that gives it
enough information to populate its UI with that,
and then allow it to then update the database
with the new information.
So I would like to do that by passing information.
In particular, I would like to pass the underlying ID
of this donut into the dialog.
That means that I'm going to pass a value that represents
that ID, this long value that represents the ID of the donut,
and I'm going to pass it from the ListFragment
into the DialogFragment.
For doing that, I'm going to use SafeArgs.
First of all, I need some library dependencies.
SafeArgs is not quite the same kind
of library module as other parts of Navigation.
It's not an API, per se, but rather a Gradle plug-in
that will generate code.
So we need to pull it in as a Gradle dependency,
and then apply the plug-in to run at build time.
Let's start by opening up the project build.gradle file
and adding in the dependency.
So I'm going to use Kotlin version 2.2.0.
You may be using a later version at that time.
It should be similar.
The main thing is to use the same version in the Navigation
component that you are using elsewhere in your project.
Now open up your app or module build.gradle file and type
this.
You're going to apply the plug-in, which
means it's going to run at build time and generate
that code for you.
So at this point, the IDE is probably
complaining that it wants you to sync, and go ahead and do that
now.
So now that we're all setup to use SafeArgs, let's go ahead
and create an argument.
Let's go over to the Navigation graph,
and we're going to add the argument
to the destination that is going to receive that argument.
So we'll click on the DialogFragment here.
We see this section over on the right that says Arguments.
Let's add one there.
Let's call it itemId.
And it asks for a Type.
The type that we use in the database is a long,
so that is probably the type that we should use here.
Note as we do this that the Nullable item on this dialog
is grayed-out.
That's because the base types allowed--
integer, Boolean, float, and long--
are backed by primitive types, which are not nullable.
So even though Kotlin's int type is nullable,
the underlying primitive type being used here is not,
so we're constrained to non-nullable types here.
Note that we're now using our dialog destination
both for entering a new item, which we've done before,
and for editing an existing item.
So we won't always have an item to pass to it.
When we're creating a new item, we
need to indicate that there is no existing item
to display in that dialog and it's
going to be accepting new information.
So we're going to pass a -1 to indicate that, since -1 is not
a valid index for the itemIds.
So let's use -1 as the Default Value here.
When we navigate to the destination with no value,
the system will supply that -1 automatically,
and we can use that to decide whether to display
an existing item or not.
Now that we've set up our destination with an argument,
let's generate the files that make it easier.
This will create the code that you can then
call to do what you want.
If you haven't run the build yet,
the file won't be generated, and Studio won't yet
know about these functions that you want to call.
So to make this a little bit easier, let's go ahead
and run a build.
So once the project is built, we can check out the results
from the generated code.
So we can navigate to the generated files
in the project list, where it says generateds.
So looking at DonutListDirections,
you can see the companion object,
which is the API that we're going to use
to navigate to this dialog.
Instead of navigating using an action, which
is what the current code does, we're
going to navigate using this NavDirections object, which
encapsulates both the action, which we originally
had that takes us to the dialog destination,
and the argument that we are passing.
Note that the function takes a long, which
is the argument we created, and supplies it
with a default value of -1, which is what
we entered into that dialog.
So if we create this without an argument,
it'll create a NavDirections object with an itemId of -1.
Clicking on the other generated file,
DonutEntryDialogFragmentArgs, we can
see that the code was generated for retrieving the argument
on the other side of this process,
once we're in our destination.
Now let's actually use these generated classes
to pass and retrieve our argument.
First, let's create the code in our DialogFragment
to get the itemId argument, and to populate the dialog
fields accordingly.
So let's navigate over to our DialogFragment.
And here, I have some code, that is handily commented out,
that does exactly that.
So that first line of code uses the property delegate Kotlin
feature, supplied by the Navigation library,
which makes retrieving the argument from a bundle easier.
It allows us to refer directly to the name of the argument
inside of that variable args that we've declared.
Then we're setting this variable editingState to these two enum
values, based on whether we have actually
supplied an argument here.
So if it turns out the itemId is greater than 0,
that means that we supplied a value
and it came in as the argument in that bundle.
If not, then it's going to be that -1 that we supplied
as a default argument, which means
that we're not editing a donut.
Instead, we are creating a new donut.
In this next code, which I will quickly uncomment,
we know that we're editing a donut, which
means that we're going to go ahead and get,
from the database, the information for the donut whose
itemId we supplied.
And that is going to provide us the information that we're
going to populate the form in the dialog with.
And now we're going to add the information about what happens
when the user clicks Done.
So when they click Done, we want to either add
or update the information in the database.
Add, if it's a new donut, or update,
if we're editing a donut.
So when the user clicks on the Done button here,
we'll come into this code, and we say, addData().
So we are calling in the view model this function called
addData() that says, first of all, is it a valid donut?.id--
basically, did we populate the ID in that donut variable with
information that we got because of that argument?
Are we editing that donut?
Or is it a new donut?
And that is going to determine in the view model
whether it is adding or updating information in the database.
And then we populate that with information
from the UI in the DialogFragment.
So now the app is able to process the new argument
and to do the right thing with it,
but we still need to send the argument through the system.
And we do that by the code where we actually
call the Navigation operation to go to the dialog destination.
So now we want to take a look at the code for the fragment
that we're navigating from, which is for the donut list.
So there's two ways that we get to that DialogFragment.
We navigate when they click on the fab,
and we navigate when they click on one
of the items in the list.
When they navigate from the fab, we use this navigate() call,
where we navigate using an action.
But if you look at the possibilities from navigate(),
there are all kinds of ways to call a navigation operation
here.
And we're going to use a different one.
So the code that was auto-generated for us
created a NavDirections object, and we're going to use that.
So DonutListDirections dot-- and then you can see,
it automatically supplies us with what we want there.
So this does not have an argument, which means,
from what we were saying before, it is automatically
going to supply an ID of -1.
So when they click on the fab, it's going to say,
yep, you're creating a new donut.
We're going to pass in -1.
If we go up here and say, OK, well,
this is what happens when they are editing one of the donuts.
So they're going to click on a donut here.
And, once again, we're going to navigate not using an action,
but using a NavDirections.
DonutListDirections dot-- and we're
going to use that same one.
But here, we are going to pass the ID of the donut
that they clicked on.
So that should be it.
Now let's run this and see what happens.
So there's our lovely donut app again.
If we click on the fab, we correctly
navigate to the dialog with no donut ID,
which means that we're going to enter a new one.
But if we click on one of the existing ones, populates it
with the existing information, we can change that information,
and it will enter that new information into the database,
and we're done.
So that's it for this episode of "Navigation."
Today, we learned about SafeArgs,
and how to encapsulate your data by using SafeArgs
to pass information very easily between different destinations
in your Navigation graph.
[MUSIC PLAYING]