[MUSIC PLAYING]
JEREMY WOODS: Hi.
My name is Jeremy Woods, and today, I
want to talk to you about Navigation
Compose on every screen size.
In 2021, the Android X Navigation Component
added the Navigation Compose module.
The Navigation Compose module allows
you to navigate between composables
while taking advantage of the infrastructure and features
already offered by the navigation component
with other components, such as views and fragments.
Navigation Compose consists of three main parts--
the Navigation Controller, which manages
the state of your navigation.
This is how you navigate between different destinations,
and it also maintains the backstack.
The Navigation Graph provides the map
for your Nav Controller.
This is where you define all of your destinations
and how they relate to one another.
And the NavHost, which is what we will focus on today.
So what is a NavHost?
The NavHost is a bounding box container
for the part of your UI that should be
considered part of navigation.
This normally takes the form of a single composable
destination, such as a screen that
takes the entire space of the NavHost.
For example, looking at this implementation
of a scaffold with a NavHost along with the image
of a device, the highlighted portion
represents the container that is the NavHost
and where the content from any composable destination
will be displayed.
So a destination being shown in the NavHost cannot take up only
part of the NavHost.
If you want destinations to take less space,
you have to make the NavHost itself take less space.
The exception for this is what we call a floating window
destination, which includes components such as dialogs
and BottomSheets.
But those are still considered a destination of the NavHost.
They just have their content displayed
in a completely separate container floating
above normal composable destinations.
The NavHost consists of everything
that navigation is responsible for showing on the screen.
But often time, Navigation interacts
with components outside of the NavHost
that Navigation is not responsible for.
This includes things like the top app bar, bottom navigation,
navigation rail, and the drawer layout.
Generally, we can identify things outside the NavHost
as something that remains on the screen
even when the destination changes.
Let's focus specifically on the bottom navigation
and navigation rail components.
Both of these contain different menu items
that users can select to indicate to Navigation
that they want to navigate to a different destination,
but they are presented in different ways.
The bottom navigation is a menu that
is presented at the bottom of the app.
For compact window sizes, you should use a bottom navigation
bar.
The navigation rail is presented on the side of the app.
For medium and expanded width window sizes,
such as tablets or foldables, you
should use a navigation rail.
Both of these components connect to Navigation
in a very similar way.
When implementing a bottom navigation component,
start by declaring your Nav Controller.
This will be our hoisted state and allow us to connect
the bottom nav to the NavHost.
Next, we will use the scaffold component
and define our bottom navigation composable
within the bottom bar parameter content.
This allows us to avoid any screen formatting
and ensures that the bottom navigation
is in the correct location.
Then, within the bottom navigation,
using the hoisted Nav Controller state,
we get the current destination.
We won't go into detail here, but we
can assume that icon is the list of items
that helps us link each icon in the bottom navigation
to a destination in the NavHost, and using the item along
with the current destination, you
can determine the correct state of the bottom navigation.
Finally, in the content of the scaffold,
we implement the NavHost, passing in the same hoisted
Nav Controller used by the bottom navigation.
So whenever the state of the Nav Controller changes,
both the bottom navigation and NavHost
are updated appropriately, and they remain in sync.
To implement a navigation rail, we just
need to change a few things.
Instead of a scaffold, we use a row.
Replace the bottom navigation with the navigation rail
and the bottom navigation item with the navigation rail item.
Then, our NavHost needs to be defined
as the next item in the row instead of in the scaffold's
content block.
With that, we've successfully gone from bottom navigation
to navigation rail.
We now have implementations of both the bottom navigation
and navigation rail.
But as it stands, there's a bit of duplicated code.
Let's see if we can clean it up a little bit.
Both functions get the current destination
from the Nav Controller.
They also need the Nav Controller
in order to navigate in response to some onClick event.
Instead of passing down the Nav Controller,
we will follow best practices and keep it hoisted.
So we'll extract out the destination and the lambda
for the onClick event.
The other major part here is the NavHost,
which, depending on the size of your graph,
can get pretty large.
And it would be much nicer if we only had
to declare that once as well.
Let's extract out our bottom nav implementation
into a composable function called BottomBarLayout.
This takes a destination, an onMenuItemSelected function,
and a content lambda function.
In the lambda of our scaffold, we invoke the content,
and everything else remains the same.
Now, how do we call our new function?
The Nav Controller piece is easy.
We just create that using rememberNavController
and pass it in.
For the NavHost, if we just pass it into the content portion,
we would still need to implement the NavHost every time
we call a new function.
So we need a way to extract it out and ensure
we use the same NavHost across different function calls.
For this, we need to declare the NavHost inside
of a movableContentOf function.
What is movable content?
Let's say you have a composable lambda
that you use multiple times during a single composition.
Each time you use that lambda, it
is a new instance unaware of any preserved state
from the previous calls.
Placing that composable lambda inside of a moveable content
wraps it in another lambda that keeps up
with the state of the original composable lambda.
Now, using the new lambda, the state of the composable lambda
is preserved each time the lambda is executed.
MovableContentOf is an experimental API
added in Compose version 1.2.0.
It allows state to move within the composition
by converting the composable lambda into a lambda that
moves the state and corresponding nodes
to any new location that is called.
When the previous call leaves the composition,
the state is temporarily preserved.
And if a new call to the lambda enters the composition,
then the state and associated nodes
are moved to the location of the new call.
If no new call is added, the state is removed permanently,
and remember, observers are notified.
For this, we can declare the NavHost inside
of a movableContent function.
Movable content allows us to declare the function just once,
and whenever the variable holding that function
is called, Compose will continue to use the same object.
So our calls to the bottom bar layout
simply calls our NavHost function
as part of the content.
We can do the same for navigation rail.
Define the navigation rail layout function
that takes a destination and onMenuItemSelected
function and the content, and keep everything
in the function the same, only replacing our NavHost
implementation for the content invocation.
Now that we've simplified our implementations
of both functions, deciding which one to use
is straightforward.
Using a when statement that takes into consideration
the width size, if the size is compact,
we should use our bottom bar layout.
Otherwise, our screen size is considered medium or expanded,
and we should use our navigation rail layout instead.
Since these components are outside of the NavHost,
they are all considered external state
and have to be managed separately
from Navigation, which is why we need
to hoist the Nav Controller.
In the future, with the use of shared elements,
it will be possible for these components
to be part of the destinations and just
shared between destinations that care about them.
We've now taken care of the components
outside of the NavHost and ensured
that we always used the proper components no matter
the screen size.
What about inside the NavHost?
Inside the NavHost consists of anything
within our bounding box.
So any interactions you do as part of creating your graph
or interactions within the composable destination that
is part of your graph, each of these composable destinations
should be able to handle every screen size.
Adapting each destination to different screen sizes
might be as simple as swapping out a list for a grid.
But sometimes, the best use experience
requires larger changes like adopting a list-detail view.
A list-detail view describes the implementation
of two screens, a list containing multiple elements
and a detail screen that corresponds to each
of the elements in the list.
With a compact window, these items
are thought of as two different composables, list
of items for the list screen and item detail
for the detail screen being stacked on top of each other,
with only one content showing at a time.
The user starts out on the list.
And once the item is selected, the window contents
are replaced with the detail view.
But with a larger window, both the list and detailed content
are displayed at the same time in one ListAndDetail
composable.
The ListAndDetail composable combine
both the list of items and item detail
composables together in the row so they are displayed side
by side.
When the user selects an item on the list view,
the detail pane updates based on the selected item.
Now, how do we combine both of our solutions to ensure
we can handle every screen size?
What if we were to combine both solutions
into a single composable, say ListDetailRoute, that
selected the proper destination based on the given window size?
If we have a large window size, we display the ListAndDetail
composable.
And if not, we display either the list
of items or item detail composables.
Which one we display depends on whether there is a currently
selected item or not.
We now have a responsible composable destination,
but there is a problem.
What happens if we are inside our ListDetailRoute
and the user wants to go back?
Well, if we're using an expanded window size,
our list and detail are inside the same composable,
so pressing back should go back to the previous destination
in the stack.
But when using the compact window size, where the detail
screen replaces the list screen, pressing back from the detail
screen should take you to the list, not
the previous destination.
Because of our separation inside the ListDetailRoute,
if we are on a smaller window size
and the item detail composable is displayed in the NavHost,
we can set a BackHandler to intercept back press
to make the ItemDetail composable
return to the list of items composable.
This type of state manipulation in the NavHost
allows our app to achieve the proper responsiveness no matter
the screen size.
But wait.
Now that your list-details are all set up,
what if you are on the detail pane
and you want to navigate to some content that replaces
your ItemDetail composable?
This is a rare scenario where you
should use a nested NavHost.
By making the detail pane its own NavHost,
you can define the destinations that should only
be reachable from the detail screen
and allow for the maintenance of a separate backstack.
If you wanted to deep link to a destination in the inner graph,
you would need to find a deep link destination
in the inner graph as well as on the parent destination that
is hoisting the nested NavHost.
This ensures that the outer graph
has a path to the destination that you
are attempting to deep link to.
Here, we cover a specific scenario
where there are a few things you should be doing in general.
The Android X Compose Material 3 Library
offers the window size class APIs,
which allow you to determine the width and/or
height of the current composable and classify it as compact,
medium, or expanded.
Ensuring you are properly hoisting state
will allow your app to disseminate
the proper information, no matter
which composable is being displayed
at any particular time.
Using the LazyVerticalGrid, LazyColumn, and LazyRow APIs
will help you take advantage of Compose's built-in support
for handling adaptive layout without needing
to do extra work on your part.
Check out our other talks, like Compose Implementing Responsive
UI for Larger Screens talk, for more ways
to make your composables responsive.
Again, these things are general practice for Compose
and not specific to Navigation.
So we need to manage the external state
outside of our NavHost by using the given window
size to determine the proper navigation
element to be displayed.
In conjunction, we should also ensure
that the inside of the NavHost is
configured so that each destination is
responsive as well.
By doing so, we can develop apps that
can truly use Navigation Compose on every screen size.
Please check out our content on developer.android.com,
and thank you.
[MUSIC PLAYING]