导航是用户与应用界面进行交互以访问内容目的地的过程。Android 的导航原则提供了一些准则,可帮助您创建一致、直观的应用导航。
响应式/自适应界面可提供响应式内容目的地,并且通常包含不同类型的导航元素,以响应显示屏尺寸的变化。例如,在小屏幕上显示底部导航栏,在中等屏幕上显示侧边导航栏,在大屏幕上显示持久性抽屉式导航栏,但响应式/自适应界面仍应遵循导航原则。
Jetpack Navigation 组件实现了导航原则,可用于协助开发具有响应式/自适应界面的应用。
响应式界面导航
应用占用的显示屏窗口大小会影响人体工学和易用性。您可以根据窗口大小类别确定适当的导航元素(例如导航栏、侧边栏或抽屉式导航栏),并将其放置在用户最容易访问的位置。根据 Material Design 布局准则,导航元素占据显示屏前缘的永久性空间,当应用的宽度较小时,导航元素可以移至底部边缘。您选择的导航元素在很大程度上取决于应用窗口的大小以及该元素必须包含的内容项数。
| 窗口大小类别 | 几项内容 | 多项内容 |
|---|---|---|
| 较小的宽度 | 底部导航栏 | 抽屉式导航栏(前缘或底部) |
| 中等宽度 | 侧边导航栏 | 抽屉式导航栏(前缘) |
| 较大的宽度 | 侧边导航栏 | 持续存在的抽屉式导航栏(前缘) |
可以通过窗口大小类别断点限定布局资源文件,以便针对不同显示屏尺寸使用不同的导航元素。
<!-- 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-w1240dp/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>
自适应内容目的地
在响应式界面中,每个内容目的地的布局都会适应窗口大小的变化。应用可以调整布局间距、调整元素位置、添加或移除内容,或更改界面元素(包括导航元素)。
如果每个目的地都能处理大小调整事件,那么更改就只会涉及到界面,不会影响应用状态的其余部分(包括导航)。
导航不应在窗口大小发生更改时才会发生。请勿仅为了适应不同的窗口大小而创建内容目的地。例如,不要针对可折叠设备的不同屏幕创建不同的内容目的地。
在窗口大小发生更改时导航到内容目的地会面临以下问题:
- 在导航到新目的地之前,可能会短暂地显示旧目的地(针对上一个窗口大小)
- 为保持可逆转性(例如,当设备折叠和展开时),您需要为每种窗口大小进行导航
- 在目的地之间保留应用状态可能很困难,因为导航可能会在弹出返回堆栈时销毁状态
此外,当窗口大小发生变化时,您的应用甚至可能未在前台运行。您的应用布局可能需要比前台应用更多的空间,当用户返回您的应用时,屏幕方向和窗口大小可能都已发生变化。
如果您的应用需要基于窗口大小的唯一内容目的地,请考虑将相关的目的地组合成包含备用自适应布局的单个目的地。
采用备用布局的内容目的地
作为响应式/自适应设计的一部分,单个导航目的地可以提供备用布局,具体取决于应用窗口大小。每个布局都会占据整个窗口,但会针对不同的窗口大小提供不同的布局(自适应设计)。
一个规范化示例为列表详情视图。对于紧凑型窗口,应用会为列表显示一个内容布局,并为详细信息显示一个内容布局。导航到列表详情视图目的地最初仅显示列表布局。选择列表项后,应用会显示详情布局来替换列表。选择返回控件后,系统会显示列表布局来替换详细信息。但是,如果窗口很大,列表和详情布局会并排显示。
借助 SlidingPaneLayout,您可以创建单个导航目的地,该目的地会在大屏幕上并排显示两个内容窗格,但在传统手机等小屏幕设备上一次只显示一个窗格。
<!-- Single destination for list and detail. -->
<navigation ...>
<!-- Fragment that implements SlidingPaneLayout. -->
<fragment
android:id="@+id/article_two_pane"
android:name="com.example.app.ListDetailTwoPaneFragment" />
<!-- Other destinations... -->
</navigation>
如需详细了解如何使用 SlidingPaneLayout 实现列表-详情布局,请参阅创建双窗格布局。
一个导航图
若要在任何设备或窗口大小中都提供一致的用户体验,请使用单个导航图,其中每个内容目的地的布局都是响应式布局。
如果您为每个窗口大小类别使用不同的导航图,那么每当应用从一个大小类别转换到另一个大小类别时,您都必须确定用户在其他导航图中的当前目的地,构建一个返回堆栈,并协调在不同导航图之间有所不同的状态信息。
嵌套导航宿主
您的应用可能包含具有自身内容目的地的内容目的地。例如,在列表-详情布局中,项目详情窗格可能包含会导航至替换项目详情的内容的界面元素。
如需实现此类子导航,请将详情窗格设为具有自己的导航图(用于指定从详情窗格访问的目的地)的嵌套导航宿主:
<!-- 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>
这与嵌套导航图不同,因为嵌套 NavHost 的导航图未连接到主导航图;也就是说,您不能直接从一个导航图中的目的地导航到另一个导航图中的目的地。
如需了解详情,请参阅嵌套导航图。
保留状态
如需提供响应式内容目的地,应用必须在设备旋转/折叠或调整应用窗口大小时保留其状态。默认情况下,诸如此类的配置更改会重新创建应用的 activity、fragment 和视图层次结构。建议通过 ViewModel 保存界面状态,二者在配置更改后保持不变。(请参阅保存界面状态 。)
大小更改应该可以逆转,例如,当用户旋转设备然后再旋转回设备时。
响应式/自适应布局可以根据不同窗口大小显示不同的内容;因此,响应式布局通常需要保存与内容相关的其他状态,即使该状态不适用于当前窗口大小也是如此。例如,某个布局只在窗口宽度较大时才会有空间显示一个额外的滚动微件。如果大小调整事件导致窗口宽度变得太小,该微件会隐藏。当应用调整到以前的大小后,滚动 widget 将再次变为可见,并且应恢复到原始滚动位置。
ViewModel 作用域
迁移到 Navigation 组件开发者指南规定了单 activity 架构,在这种架构中,目的地作为 fragment 实现,并使用 ViewModel 实现数据模型。
ViewModel 的作用域始终限定为某个生命周期,一旦该生命周期永久结束,ViewModel 就会被清除,并且可以被舍弃。ViewModel 的生命周期作用域以及 ViewModel 的共享范围取决于使用哪个属性委托来获取 ViewModel。
在最简单的情况下,每个导航目的地都是一个具有完全隔离界面状态的 fragment;因此,每个 fragment 都可以使用 viewModels() 属性委托来获取作用域限定为该 fragment 的 ViewModel。
如需在 fragment 之间共享界面状态,请通过在 fragment 中调用 activityViewModels() 将 ViewModel 的作用域限定为相应 activity(Activity 的等效项就是 viewModels())。这样,该 activity 以及附加到其上的任何 fragment 就能共享 ViewModel 实例。不过,在单 activity 架构中,此 ViewModel 作用域只要应用存在,就会持续有效,因此即使没有任何 fragment 使用 ViewModel,该 ViewModel 也会保留在内存中。
假设您的导航图中有一系列 fragment 目的地,代表一个结账流程,并且整个结账体验的当前状态是处于在 fragment 之间共享的 ViewModel。将 ViewModel 的作用域限定为相应 activity 不仅过于宽泛,而且实际上还暴露了另一个问题:如果用户针对一笔订单完成结账流程,然后针对第二笔订单再次完成该流程,那么这两个订单将使用相同的结账 ViewModel 实例。在第二笔订单结账之前,您必须手动清除第一笔订单中的数据。任何错误都可能会给用户带来巨大损失。
请将 ViewModel 的作用域限定为当前 NavController 中的导航图。创建一个嵌套导航图来封装属于结账流程的目的地。然后,在每个 fragment 目的地中,使用 navGraphViewModels() 属性委托并传递导航图的 ID 以获取共享的 ViewModel。这样可确保在用户退出结账流程且嵌套导航图超出作用域后,相应 ViewModel 实例会被舍弃,并且不会用于下一次结账。
| 范围 | 属性委托 | ViewModel 可共享的对象 |
|---|---|---|
| Fragment | Fragment.viewModels() |
仅 fragment |
| 活动 | Activity.viewModels() 或 Fragment.activityViewModels() |
activity 和连接到它的所有 fragment |
| 导航图 | Fragment.navGraphViewModels() |
同一导航图中的所有 fragment |
请注意,如果您使用的是嵌套导航宿主(请参阅嵌套导航宿主部分),则在使用 navGraphViewModels() 时,该宿主中的目的地不能与该宿主外部的目的地共享 ViewModel 实例,因为导航图没有连接起来。在这种情况下,您可以改用 activity 作用域。