-
根据迁移到 AndroidX 文档,您的
gradle.properties
文件中还必须具有android.useAndroidX=true
。 -
官方对
Navigation
的设计是不会对Fragment
进行状态保存,需要开发者在ViewModel实现,所以无论是跳转到下一页面或者回退到上一页面,Fragment
都会重新走生命周期方法,对此,本文也给出了解决方案,参考提供安全可靠的 Navigation 操作。
基本使用
在Project的 build.gradle 文件添加以下依赖:
buildscript {
repositories {
google()
}
dependencies {
def nav_version = “2.3.5”
classpath “androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5”
}
}
在app的 build.gradle 文件添加以下依赖:
apply plugin: “androidx.navigation.safeargs”
dependencies {
// Kotlin相关依赖
implementation “androidx.navigation:navigation-fragment-ktx:2.3.5”
implementation “androidx.navigation:navigation-ui-ktx:2.3.5”
}
创建导航图:
-
在“Project”窗口中,右键点击
res
目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。 -
在 File name 字段中输入名称,例如“nav_graph”。
-
从 Resource type 下拉列表中选择 Navigation,然后点击 OK。
当添加首个导航图时,Android Studio 会在 res
目录内创建一个 navigation
资源目录。该目录包含您的导航图资源文件(例如 nav_graph.xml
)。
添加nav_graph.xml
后,Android Studio 会在 Navigation Editor 中打开该文件。在 Navigation Editor 中,您可以直观地修改导航图,或直接修改底层 XML。
下一步我们需要点击+号,去创建一个destination
,点击Create new destination
创建 HomeFragment
。
destination是目的地,即你想要去的地方。可以是Fragment或者Activity,但最常见的是Fragment,Navigation组件的目的就是方便开发者在一个Activity中管理多个Fragment。
这时候就会生成一个Fragment,右上角默认为startDestination
即NavHostFragment
容器首先要展示的Fragment。如果发现页面不能预览,可以手动编辑XML文件在fragment
节点添加页面对应的布局引用就可以了(例如 tools:layout="@layout/fragment_home"
)。
点击右上角Split
可以看到XML文件。这里的<navigation>
元素是导航图的根元素。当我们向导航图添加目的地和连接操作时,可以看到相应的 <destination>
和 <action>
元素在此处显示为子元素。如果您有嵌套视图,它们将显示为子 <navigation>
元素。
向 Activity 添加 NavHost
导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在我们的应用中导航时,目的地会在该容器中交换进出。
导航宿主必须派生于 NavHost
。Navigation 组件的默认 NavHost
实现 (NavHostFragment
) 负责处理 Fragment 目的地的交换。
Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment
。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。
接下来我们在MainAcitivty的XML文件添加 NavHostFragment,这里推荐使用的是androidx.fragment.app.FragmentContainerView
而非fragment
<androidx.fragment.app.FragmentContainerView
android:id=“@+id/nav_host_fragment”
android:name=“androidx.navigation.fragment.NavHostFragment”
android:layout_width=“0dp”
android:layout_height=“0dp”
app:layout_constraintLeft_toLeftOf=“parent”
app:layout_constraintRight_toRightOf=“parent”
app:layout_constraintTop_toTopOf=“parent”
app:layout_constraintBottom_toBottomOf=“parent”
app:defaultNavHost=“true”
app:navGraph=“@navigation/nav_graph” />
-
android:name
属性指向NavHost
实现的类名称。 -
app:navGraph
属性将NavHostFragment
与导航图相关联。导航图会在此NavHostFragment
中指定用户可以导航到的所有目的地。 -
app:defaultNavHost="true"
属性确保您的NavHostFragment
会拦截系统返回按钮。请注意,只能有一个默认NavHost
。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认NavHost
。
向导航图添加目的地
我们可以从现有的 Fragment 或 Activity 创建目的地。还可以使用 Navigation Editor 创建新目的地,或创建占位符以便稍后替换为 Fragment 或 Activity。
连接目的地
可以使用 Navigation Editor 将两个目的地连接起来,具体操作步骤如下:
-
在 Design 标签页中,将鼠标悬停在目的地的右侧,该目的地为您希望用户从中导航出来的目的地。该目的地右侧上方会显示一个圆圈;
-
点击希望用户导航到的目的地,并将光标拖动到该目的地的上方,然后松开。这两个目的地之间生成的线条表示操作。
点击右上角Code
切换到XML视图可以看到:
<navigation xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”
android:id=“@+id/nav_graph”
app:startDestination=“@id/homeFragment”>
<fragment
android:id=“@+id/homeFragment”
android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.HomeFragment”
android:label=“HomeFragment”
tools:layout=“@layout/fragment_home”>
<action
android:id=“@+id/action_homeFragment_to_categoryFragment”
app:destination=“@id/categoryFragment” />
<fragment
android:id=“@+id/categoryFragment”
android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment”
android:label=“CategoryFragment”
tools:layout=“@layout/fragment_category”/>
在导航图中,连接由 <action>
元素表示。连接至少应包含<action>
的 ID 和用户应转到的目的地的 ID(destination
指向的id
)。
导航到目的地
导航到目的地是使用 NavController
完成的,它是一个在 NavHost
中管理应用导航的对象。每个 NavHost
均有自己的 NavController
。您可以使用以下方法之一获取 NavController
:
Kotlin:
-
Fragment.findNavController()
.findNavController()") -
Activity.findNavController(viewId: Int)
.findNavController(kotlin.Int)")
Java:
在目的地之间传递数据
启用 Safe Args 后,会为每个action
的起点和终点生成类型安全的类和方法。具体操作如下:
-
在 Navigation Editor 中,点击接收参数的终点;
-
在 Attributes 面板中,点击 Add ( + );
-
在显示的 Add Argument Link 窗口中,输入参数名称、参数类型、参数是否可为 null,以及默认值(如果需要);
-
点击 Add。请注意,该参数现在会显示在 Attributes 面板的 Arguments 列表中;
-
您还可以看到该参数已添加到 XML 中。点击 Code 标签页以切换到 XML 视图,就会发现您的参数已添加到接收该参数的目的地。相关示例如下所示:
<fragment
android:id=“@+id/categoryFragment”
android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment”
android:label=“CategoryFragment”
tools:layout=“@layout/fragment_category”>
<argument
android:name=“pageType”
app:argType=“integer” />
除了可以在XML文件中静态传值,还可以动态设置。我们在起点通过NavController
的navigate()
导航,并且使用HomeFragmentDirections.actionHomeFragmentToCategoryFragment(99)
进行传参,然后在终点通过CategoryFragmentArgs
获取传过来的参数:
//起点设置参数 类型安全
Navigation.findNavController(v).navigate(HomeFragmentDirections.actionHomeFragmentToCategoryFragment(99))
//终点接收参数 类型安全
val args : CategoryFragmentArgs by navArgs()
println(args.pageType)
踩坑:当我们快速点击按钮会重复调用navigate()
导致异常如下:
java.lang.IllegalArgumentException: Navigation action/destination com.soushin.tinmvvm:id/action_homeFragment_to_categoryFragment cannot be found from the current destination Destination(com.soushin.tinmvvm:id/categoryFragment) label=CategoryFragment class=com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment
这是因为第一次调用navigate()
后当前页面已经换了,而我们的action
是明确了起点和终点的,所以当第二次调用的时候起点不再是HomeFragment
就会报上述异常,这在页面跳转时添加转场动画会显得非常明显(转场动画有duration),只需要在跳转前判断currentDestination
是否是当前页面的destination
即可。示例如下:
if (Navigation.findNavController(v).currentDestination?.id != R.id.homeFragment) return
使用NavigationUI更新页面
Navigation 组件包含 NavigationUI
类。此类包含多种静态方法,可帮助您使用Toolbar
、NavigationView
、BottomNavigationView
、CollapsingToolbarLayout
、DrawerLayout
来管理导航。
顶部标题栏(Toolbar/ActionBar/CollapsingToolbarLayout)
利用 NavigationUI
内置的方法,我们可以在用户浏览应用的过程中自动更新顶部标题栏中的内容。例如,NavigationUI
可使用导航图中的目的地标签及时更新顶部标题栏的标题。
<fragment …
android:label=“Page title”>
…
还有个语法糖就是,如果我们按下面介绍的顶部标题栏实现方法使用 NavigationUI
,可以在标签中使用 {argName}
格式,从而根据提供给相应目的地的参数来自动填充附加到目的地的标签。
NavigationUI
支持以下顶部标题栏类型:
AppBarConfiguration
NavigationUI
使用 AppBarConfiguration
对象管理在应用显示区域左上角的导航按钮的行为。导航按钮的行为会根据当前页面是否位于顶层目的地而变化。
顶层目的地是一组存在层次关系的目的地中的根级或最高级目的地。顶层目的地不会在顶部标题栏中显示“返回”按钮,因为不存在更高等级的目的地。默认情况下,应用的目的地是唯一的顶层目的地。
当用户位于顶层目的地时,如果目的地使用了 DrawerLayout
,导航按钮会变为抽屉式导航栏图标。如果目的地没有使用 DrawerLayout
,导航按钮处于隐藏状态。当用户位于任何其他目的地时,导航按钮会显示为返回按钮。在配置导航按钮时,如需将起始目的地用作唯一顶层目的地,请创建 AppBarConfiguration
对象并传入相应的导航图,如下所示:
val appBarConfiguration = AppBarConfiguration(navController.graph)
在更多情况下,我们可能需要定义多个顶层目的地,而不是使用默认的目的地。这种情况的一种常见用例是 BottomNavigationView
,在此场景中,同级屏幕可能彼此之间并不存在层次关系,并且可能各自有一组相关的目的地。对于这样的情况,您可以改为将一组目的地 ID 传递给构造函数,如下所示:
val appBarConfiguration = AppBarConfiguration(setOf(R.id.main, R.id.profile))
创建Toolbar
如需使用 NavigationUI
创建工具栏,请先在主 activity 中定义该工具栏,如下所示:
<androidx.appcompat.widget.Toolbar
android:id=“@+id/toolbar” />
<androidx.fragment.app.FragmentContainerView
android:id=“@+id/nav_host_fragment”
… />
…
接下来,从主 activity 的 onCreate()
方法中调用 setupWithNavController()
"),如下所示:
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
…
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(navController.graph)
findViewById(R.id.toolbar)
.setupWithNavController(navController, appBarConfiguration)
}
注意:使用 Toolbar
时,Navigation 组件会自动处理导航按钮的点击事件,因此我们不需要重写 onSupportNavigateUp()
")。
如需将导航按钮配置为在所有目的地都显示为返回按钮,那么在构建 AppBarConfiguration
时,请为顶层目的地传递一组空白目的地 ID。例如,如果我们有第二个 activity,其应在所有目的地的 Toolbar
中显示返回按钮,这样做可能就很有用。这样一来,当返回堆栈上没有其他目的地时,用户便可导航回父 activity。您可以使用 setFallbackOnNavigateUpListener()
") 控制在 navigateUp()
不另行执行任何操作时的回退行为,如以下示例所示:
override fun onCreate(savedInstanceState: Bundle?) {
…
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(
topLevelDestinationIds = setOf(),
fallbackOnNavigateUpListener = ::onSupportNavigateUp
)
findViewById(R.id.toolbar)
.setupWithNavController(navController, appBarConfiguration)
}
使用CollapsingToolbarLayout
如需在工具栏中添加 CollapsingToolbarLayout
,请先在 activity 中定义工具栏和周围布局,如下所示:
<com.google.android.material.appbar.AppBarLayout
android:layout_width=“match_parent”
android:layout_height=“@dimen/tall_toolbar_height”>
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id=“@+id/collapsing_toolbar_layout”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
app:contentScrim=“?attr/colorPrimary”
app:expandedTitleGravity=“top”
app:layout_scrollFlags=“scroll|exitUntilCollapsed|snap”>
<androidx.appcompat.widget.Toolbar
android:id=“@+id/toolbar”
android:layout_width=“match_parent”
android:layout_height=“?attr/actionBarSize”
app:layout_collapseMode=“pin”/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id=“@+id/nav_host_fragment”
… />
…
接着,通过主 activity 的 onCreate
方法调用 setupWithNavController()
"),如下所示:
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
…
val layout = findViewById(R.id.collapsing_toolbar_layout)
val toolbar = findViewById(R.id.toolbar)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(navController.graph)
layout.setupWithNavController(toolbar, navController, appBarConfiguration)
}
使用ActionBar
如需向默认操作栏添加导航支持,请通过主 activity 的 onCreate()
方法调用 setupActionBarWithNavController()
"),如下所示。请注意,您需要在 onCreate()
之外声明 AppBarConfiguration
,因为您在替换 onSupportNavigateUp()
时也使用该方法:
private lateinit var appBarConfiguration: AppBarConfiguration
…
override fun onCreate(savedInstanceState: Bundle?) {
…
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
}
接着,替换 onSupportNavigateUp()
以处理向上导航:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
最后
在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
figuration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
}
接着,替换 onSupportNavigateUp()
以处理向上导航:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
最后
在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-S3HOZ9HA-1715413734212)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!