[MUSIC PLAYING]
IAN LAKE: Hi.
My name is Ian Lake, and I'm an engineer on the Android
Architecture Components team, and I'd
like to share a few tips for Navigating Navigation.
Specifically, I'll share a few of the best practices
around using the Navigation Architecture Component and how
recently added functionality can make
some common tasks and some tricky cases much easier
to handle.
The Navigation Architecture Component
serves as a framework for navigating within your app.
It consists of three major pieces.
You can think of the Navigation Graph as the map of your app.
In Android Studio's Navigation Editor,
you can visualize each of the screens
as destinations in your graph.
Actions link these destinations together
and denote how the user can navigate between destinations.
The Navigation Editor's split mode
lets you see the visual graph of your destinations
and the Navigation XML file it's based on side by side,
but Navigation also supports programmatically
building your Navigation graph via a Kotlin DSL.
Either way, that graph needs to be associated with a NavHost
to actually be displayed to users.
The NavHost serves as the container within your activity
where the destinations in your graph are shown.
This provides a clear separation between global UI,
such as a bottom navigation bar or a navigation drawer,
and the views associated with just a single destination.
And, finally, the NavController is the heart of the Navigation
Architecture Component.
Each NavHost has a separate NavController
that is responsible for managing its destination back stack,
and it's the NavController that you
use to navigate between destinations
and listen for Navigation events.
Another key feature the NavController enables
is the ability to deep-link into a specific destination.
An Android app isn't just a single entry point
from a launcher icon.
It has notifications, app widgets, conversation bubbles,
app shortcuts, and web URLs that should all
take the user directly to where they need to go.
Because deep-linking is such an important foundational piece
for making your app feel like an integrated part of the user's
device, Navigation has made significant progress
in expanding the types of deep links supported.
Associating one or more URIs with the destination
was the first type of deep link supported.
While the first release of Navigation
focused on the path part of the URI,
Navigation 2.2 expanded to also support
arbitrary query parameters, allowing
you to extract arguments from the values provided there.
But URIs aren't the only type of deep link
the Android system uses.
Take, for example, the ActionSend intent.
This type of intent is used by camera and gallery apps
to share an image out to other apps.
It requires that your apps say that it
supports that specific action along with what
MIME types you support.
With Navigation 2.3, deep-link support
has been further expanded to support deep links that
rely on actions and MIME types.
To take advantage of this functionality,
you add the deep-link element to your destination just like with
a URI, but now you use the App:action and App:MIMEtype
attributes.
Then your destination can check whether it
was triggered by that deep link by looking at the Key Deep Link
Intent.
If the action matches, you can extract
the URI of the image shared to your app.
The perhaps most conspicuous part
is what you don't have to write.
There's no logic needed in your activity at all.
Navigation handles routing the user
to the correct destination, and all the logic
needed to handle the incoming deep link
is encapsulated in that single destination.
So deep links provide a way to get users
to the correct place in your app with the information,
in the form of arguments, needed to provide the right context.
But what if that's not enough?
What if that destination requires
that the user is logged in?
Deep links are one of the reasons why
the principles of Navigation specifically
state that Login shouldn't be used as the start
destination of your app.
Deep links mean your app has multiple entry points,
so users won't always see your start destination
as their first screen.
Android itself will restore users
back to exactly where they were, even if they
deleted their account or changed their password online,
invalidating their login, and, not to mention,
that your start destination is what's automatically
put on the back stack when you deep-link.
Hitting Back and seeing your Login screen isn't a good look.
So what are you supposed to do?
Well, if you can be restored at any destination by the Android
system, then each destination that requires login needs
to conditionally-- i.e., only when necessary--
navigate to your Login screen and ask the user to log in.
If the user logs in successfully,
the Login screen gets popped off the back stack
and, ta-da, you're back where you
were with no state being lost.
But what if the user chooses not to log in at all?
This is where Navigation 2.3's ability to return a result
can be useful to distinguish these cases.
Let's take a look at what this logic looks like, starting
from your Login destination.
So our Login fragment can be called from potentially many
destinations.
How do we know where to return a result to?
Here's where the Navigation 2.3 API
of previous back-stack entry comes in handy.
It will always return the previous Nav back-deck entry,
the class that encapsulates all of the NavController-specific
state for the previous destination.
From there, we can get a reference
to its SaveStateHandle.
It provides us with a key value map that
has two important qualities.
It survives configuration changes as well as
process death and recreation, so you never lose results.
And it's observable-- a fact our other destination
will take advantage of.
So with the SaveStateHandle of the previous destination,
we can first set an initial state.
This is the state that we want to return
if the user were to immediately hit the system Back button.
Here, we'll set the Login Successful value to false.
Now, after some view-binding code and server-verification
code that has nothing to do with navigation,
we finally get to the point where the user has successfully
logged in and we've updated the user repository
with the new user information.
We do two things at this point.
We set our Login Successful value to true,
and we call PopBackStack to return to where we came from.
That's all we need to do on the Login destination side.
Let's see what needs to be done on the other side.
So on our other destination, let's say the EditProfile
fragment, we require a login.
How you structure your checks on whether the user is logged in
is up to you, but let's assume there is a user repository that
delivers a flow we can collect to get our current user.
If the user is 0, we redirect users to the Login destination.
Just a simple Navigate call is enough here.
To listen for results, we can use the other helper method
in Navigation 2.3--
CurrentBackStackEntry.
Since we want to get the results out of our back-stack entry,
we get the same SaveStateHandle that the other destination
is going to put the result into, and we call GetLiveData
to get an observable stream.
The callback here won't be triggered
until the other destination puts a result in
and we return back to this destination.
What you do in your callback on failure
is a business decision on your part.
You could consider making an overlay visible
explaining why login is necessary,
just finish the whole activity if every screen requires login,
or do what we do here and navigate back
to the root of our graph, resetting the users back
to our start destination.
This would be appropriate if your start destination does not
require login.
I know.
There's a lot going on behind the scenes.
Now, we've written tests for Navigation's logic,
but it was also incredibly important to us
to enable you to write tests for your destination's logic.
A Navigation testing library, released in Navigation 2.3,
provides a test fake in TestNavHostController.
The TestNavHostController contains
all the same NavController internal logic,
which means its behavior is always
going to mirror the real thing.
But it differs in one important case--
it doesn't actually navigate.
The TestNavHostController state will be updated,
but it doesn't complicate your test
by actually doing fragment transactions or animations,
ensuring you a fast and reliable base for your tests.
So let's see how this would work with our login fragment
and ensure we can test our initial state using
fragment scenario and TestNavHostController.
We'll start by creating our TestNavHostController
and calling SetViewModelStore to ensure we can
use the SaveStateHandle API.
Since this acts just like a real NavController,
we still need to call SetGraph on it,
passing it our real graph.
However, we also get some helpful utilities
like SetCurrentDestination that allows us to jump directly
to our EditProfile fragment.
Then we navigate to the Login fragment we want to test.
When testing our fragment in isolation,
our test takes on the role of the NavHost fragment,
setting our TestNavHostController right
when the view is created.
Our test for the initial state is then straightforward.
We get the previous back-stack entry from the NavController
and check whether the value is already set to false.
You could extend this with a simple Espresso test
to actually do the login and ensure that the state swaps
over to true.
Importantly, we didn't need to create fragile mocks,
and we didn't need to warp our code
to test your business logic's intersection with Navigation.
Letting you own your unique business logic
is a really important factor for Navigation.
But for some cases, it really is just boilerplate.
Dynamic Feature modules are a really powerful way
to deliver features just in time through Google Play.
But if those features involve one or more screens
in your app, coordinating the download,
install, and display of those screens
isn't necessarily straightforward,
unless you're using Navigation 2.3's support for Dynamic
Feature modules.
If you're already using a NavHost fragment,
adding support for destinations provided by a Dynamic Feature
module involves just a few steps.
First, add the NavigationDynamicFeatures
fragment artifact to your build.gradle file.
Then swap out your NavHost fragment
for a DynamicNavHost fragment.
With only those two changes, you can now
add destinations from Dynamic Feature modules
to your Navigation graph.
Just add the module name attribute,
and Navigation will take care of installing your module,
if needed, before navigating to your destination via a progress
destination.
Loading a whole Navigation graph is also
supported via the IncludeDynamic tag.
While there's a lot of new features
already in work for future versions of Navigation,
everything I've talked about here
is stable and available right now.
So look through our updated documentation
on developer.android.com, and check out the releases page
for more details and links to the issue tracker
for filing your own feature requests.
Thanks.
[MUSIC PLAYING]