Navigation refers to the interactions that allow users to navigate across, into, and back out from the different pieces of content within your app. Creating a responsive UI doesn't fundamentally change the process of navigation, and you should still follow all of the principles of navigation. The Jetpack Navigation component adheres to these principles and can continue to be used in apps with highly responsive layouts.
Create responsive navigation views
Because the size of an app impacts ergonomics and usability, use window size classes to decide where to place the navigation element where it is most accessible. In the Material Design layout guidelines, navigation elements occupy a persistent space on the leading edge and can move to the bottom edge when the app's width is compact. Which navigation element you use largely depends on whether or not you have more items than can fit in a bottom navigation bar view or a navigation rail.
|Few destinations||Many destinations|
|compact width||bottom navigation view||navigation drawer (leading edge) or
navigation drawer (bottom)
|medium width||navigation rail||navigation drawer (leading edge)|
|expanded width||navigation view||navigation view|
The following snippet shows three layout files for the same activity with each using a different view to facilitate navigation. The qualified layouts use the width breakpoints described earlier.
<!-- res/layout/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.bottomnavigation.BottomNavigationView android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" ... /> <!-- content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout>
<!-- res/layout-w600dp/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.navigationrail.NavigationRailView android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" ... /> <!-- content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout>
<!-- res/layout-w840dp/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.navigation.NavigationView android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" ... /> <!-- content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout>
Use a single navigation graph
It might seem natural to use a different navigation graph for different size classes, but this presumes that the app's window remains a fixed size. On the other hand, responsive UI presumes that an app's window can change size at any time, and the UI should adapt appropriately.
With parallel navigation graphs, whenever the app transitions to another size class, you have to determine the user's current destination in the other graph, construct a back stack, and reconcile other state information that differs between the graphs. This is complicated and prone to error.
Similarly, navigation events should not occur as a side effect of a size change. Doing so can cause similar problems to those described above. A good analogy is a web browser: it would be very strange if the browser went back a page, redirected to another page, or changed the history when you resize the browser window.
Instead, use a single navigation graph that drives the user experience on any device, and make each destination capable of adapting to the current size. This reinforces the major principles of responsive UI: flexibility and continuity. If each individual destination gracefully handles resize events, then changes are isolated to only the UI, and the rest of the app state (including navigation) is preserved, which aids continuity. See Migrate your UI to responsive layouts for more about building responsive layouts.
When starting with a phone-centric design, one attractive adaptation for large screens is to display two destinations at once, usually with the List Detail canonical layout. For this to work with a single navigation graph, these two destinations must be combined into one new destination that is capable of displaying each individually on small screens or both together on large. The new destination replaces both of the old destinations in the navigation graph.
<!-- res/navigation/main_navigation.xml --> <navigation ...> <fragment android:id="@+id/article_list" android:name="com.example.app.ArticleListFragment" /> <fragment android:id="@+id/article_detail" android:name="com.example.app.ArticleDetailFragment" /> <!-- other destinations ... --> </navigation>
<!-- res/navigation/main_navigation.xml --> <navigation ...> <!-- replaces article_list and article_detail --> <fragment android:id="@+id/article_two_pane" android:name="com.example.app.ArticleTwoPaneFragment" /> <!-- other destinations ... --> </navigation>
See Create a two pane layout for details on
implementing a List Detail layout using
Nested navigation host
You may have a destination in which one region of the UI can show multiple content states that could be distinct destinations. For example, in a List Detail layout, the detail pane could show primary information about the currently selected item, and tapping on an element drills down further by replacing the detail pane's content.
In these cases, to facilitate this sub-navigation, the detail pane can be a nested navigation host with its own graph containing just these content states as destinations. This is different from a nested navigation graph because the graphs are not connected; that is, you cannot navigate directly from destinations in one graph to destinations in the other.
Here's a sample layout that implements a List Detail destination, where the detail pane is a nested navigation host with its own separate graph:
<!-- layout/two_pane_fragment.xml --> <androidx.slidingpanelayout.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/sliding_pane_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list_pane" android:layout_width="280dp" android:layout_height="match_parent" android:layout_gravity="start"/> <!-- Detail pane is a nested Navigation Host. Its graph is not connected to the main graph that contains the two_pane_fragment destination. --> <androidx.fragment.app.FragmentContainerView android:id="@+id/detail_pane" android:layout_width="300dp" android:layout_weight="1" android:layout_height="match_parent" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/detail_pane_nav_graph" /> </androidx.slidingpanelayout.widget.SlidingPaneLayout>
Follow the guidelines on this page for any nested navigation hosts in addition to your main navigation host.
Preserve the user's state
Making destinations responsive requires preserving the user's state when the device is rotated or folded, or the app's window is resized. By default, these changes recreate your Activity, Fragment, and View hierarchy. The recommended way to save UI state is with a ViewModel, which survives across configuration changes.
An important consideration is that size changes should be reversible, such as if the user rotates the screen and then rotates it back. Responsive layouts might display different pieces of content at different window sizes, and so they often need to save additional state related to that content, even if it might not be applicable at the current window size.
For example, suppose a layout might have space to show an additional scrolling widget only at larger window widths. If a resize causes the window width to become too small, the widget is hidden. When the app resizes back, the scrolling widget becomes visible again, and ideally the original scroll position is restored.
Our guide to navigation promotes a single-Activity architecture in which destinations are implemented as Fragments and their data models are implemented as ViewModels. A ViewModel is always scoped to a lifecycle, and once that lifecycle goes away permanently, the ViewModel is cleared and can be discarded. Which lifecycle, and therefore how broadly the ViewModel can be shared, depends on which property delegate is used to obtain it.
In the simplest case, every destination is a single Fragment with a completely
isolated UI state. In that case, each Fragment can use the
property delegate and obtain a
ViewModel scoped to that Fragment.
Many apps require sharing some UI state among two or more Fragments. One
option is to scope the ViewModel to the Activity by using
in the Fragment; the equivalent for Activity is just
This allows the Activity and any Fragments that attach to it to share the
ViewModel instance. However, in a single-Activity architecture, this scope
lasts effectively as long as the user is using the app, so it's possible for
the ViewModel to remain in memory even if nothing currently uses it.
Suppose your navigation graph has a sequence of Fragment destinations representing a checkout flow. The current state for the entire checkout experience is in a ViewModel that is shared among them. Scoping this ViewModel to the Activity is not only too broad, but actually exposes another problem: if the user goes through the checkout flow for one order, and then goes through it again for a second order, both times will use the same instance of the checkout ViewModel. You will have to manually clear data in the ViewModel that was left behind from the first checkout, and a mistake could be costly for the user.
Instead, you can scope the ViewModel to a navigation graph in the current
NavController. Create a
nested navigation graph to
encapsulate the destinations that are part of the checkout flow. Then, in each
of those Fragment destinations, use the
property delegate and pass the ID of the navigation graph to obtain the shared
ViewModel. This ensures that once the user exits the checkout flow and this
scope is finished, the corresponding instance of the ViewModel is also
discarded and will not be used for the next checkout.
|Scope||Property delegate||Can share ViewModel with|
||Current Fragment only|
||Activity and all Fragments attached to it|
||All Fragments in the same navigation graph|
Note that if you are using a
nested navigation host, then destinations in that
host cannot share ViewModels with destinations outside that host when using
navGraphViewModels() because the graphs are not connected. You can fall back
to using the Activity's scope in this case.