Navigation for responsive UIs

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.

Combined destinations

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.

Before:

<!-- 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>

After:

<!-- 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 SlidingPaneLayout.

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.

ViewModel scopes

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 viewModels() 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 activityViewModels() in the Fragment; the equivalent for Activity is just viewModels(). 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 navGraphViewModels() 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
Fragment Fragment.viewModels() Current Fragment only
Activity Activity.viewModels()

Fragment.activityViewModels()

Activity and all Fragments attached to it
Navigation Graph Fragment.navGraphViewModels() 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.