Android Jetpack架构组件(九)— Navigation

一、Navigation简介

Navigation 直接翻译即为导航,它是 Android Jetpack 组件之一。
导航是指支持用户导航、进入和退出应用中不同内容片段的交互。Android Jetpack 的导航组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。

Navigation 让单 Activity 应用成为首选架构。应用内Fragment页面的跳转则由 Navigation 来处理,开发者无需在处理 FragmentTransaction 的复杂性以及相关的转场动画。

二、Navigation使用

1、在app的gradle.build中添加依赖

dependencies {
  def nav_version = "2.5.0"

  // Java language implementation
  implementation "androidx.navigation:navigation-fragment:$nav_version"
  implementation "androidx.navigation:navigation-ui:$nav_version"

  // Kotlin
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

  // Feature module Support
  implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

  // Testing Navigation
  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"

  // Jetpack Compose Integration
  implementation "androidx.navigation:navigation-compose:$nav_version"
}

2、创建导航图xml文件

在需要创建的module中选择res,创建 Android Resource File


2.png

在弹出的窗口中。设置名称。选择 Navigation 即可在 res/navigation下创建出一个开发者设置的导航图

3.png

创建的nav_graph.xml文件

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph">

</navigation>

3、创建导航图内容

4.png
通过 Navigation Graph (导航图) 提供的快捷方式去添加。

点击添加目的地小图标 Create new desination 即可创建新的 desination 即(目的地), 当然还可以从列表直接选择之前已经创建好的。


5.png
<?xml version="1.0" encoding="utf-8"?>
<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.alan.mynavigation.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/secondFragment"
        android:name="com.alan.mynavigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />

</navigation>

xml字段含义

navigation标签

字段 含义
android:id="@+id/nav_graph" 创建的 xml 名称
app:startDestination 表示起始导航图,会显示哪个界面,会有一个小房子标志

fragment标签

字段 含义
android:name 目的地对应的类
android:lable 包含该目的地的 XML 布局文件的名称
tools:layout 设置可以预览导航图目的地布局文件

4、将导航图添加到布局当中

在MainActivity的xml中添加 FragmentContainerView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fcv_nav"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

FragmentContainerView标签

字段 含义
android:id 必填,否则报错
android:name 属性包含 NavHost 实现的类名称。
app:navGraph 属性将 NavHostFragment 与导航图相关联
app:defaultNavHost="true" 确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。若为true,需要的Activity中重写onSupportNavigateUp方法。
override fun onSupportNavigateUp(): Boolean {
  //调用扩展函数findNavController
  return findNavController(R.id.nav_host_fragment).navigateUp()
}  

5、普通跳转并传参数

1.创建跳转行为

切换到Design 标签页中,将鼠标悬停在目的地的右侧,该目的地右侧上方会显示一个圆圈,点击您希望用户导航到的目的地,并将光标拖动到该目的地的上方,然后松开。这两个目的地之间生成的线条表示操作完成,并在xml 中会在起点的fragment下生成一个<action>标签,其中 app:destination表示目的地,design页面右边属性也可看到详情。

<fragment
        android:id="@+id/homeFragment"
        android:name="com.alan.mynavigation.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
12.gif
2.无参跳转
  • Fragment.findNavController().navigate(ResId)
//点击事件
btn_second.setOnClickListener {
  //通过传入action来实现导航 
  findNavController().navigate(R.id.action_homeFragment_to_secondFragment)
}
  • View.findNavController().navigate(ResId)
//点击事件
btn_second.setOnClickListener {
  //通过传入action来实现导航
  it.findNavController().navigate(R.id.action_homeFragment_to_secondFragment)
}
  • Activity.findNavController().navigate(ResId)
//点击事件
btn_second.setOnClickListener {
  //通过传入action来实现导航 
  findNavController().navigate(R.id.action_homeFragment_to_secondFragment)
}

其中的 resId 是上面我们创建的<action>标签中的id

获取NavController方式不同
//注意:Kotlin使用和Java中获取NavController方式不同
//Kotlin:
Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)

//Java:
NavHostFragment.findNavController(Fragment)
Navigation.findNavController(Activity, @IdRes int viewId)
Navigation.findNavController(View)


//使用 FragmentContainerView 创建 NavHostFragment,或通过 FragmentTransaction 手动将 NavHostFragment 添加到您的 Activity 时,尝试通过 Navigation.findNavController(Activity, @IdRes int) 检索 Activity 的 onCreate() 中的 NavController 将失败。您应改为直接从 NavHostFragment 检索 NavController。
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController

3.带参跳转

不推荐的方式。推荐的方式可以参考 safe ages跳转传参。

//点击事件
btn_second.setOnClickListener {
  val bundle = Bundle()
  bundle.putString("username", "alan")
  bundle.putInt("age", 18)
  //通过传入action来实现导航
  it.findNavController().navigate(R.id.action_homeFragment_to_secondFragment,bundle)
}

获取的时候和Fragment以前的方式一样,使用 arguments 去获取。

6、safe ages跳转传参

Navigation 组件具有一个名为 Safe Args 的 Gradle 插件,该插件可以生成简单的 object 和 builder 类,以便以类型安全的方式浏览和访问任何关联的参数。我们强烈建议您将 Safe Args 用于导航和数据传递,因为它可以确保类型安全。

1.配置

在根目录build.gradle中先添加 plugin

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.3.2"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

必须应用以下两个可用插件之一,请将以下行添加到应用或模块的 build.gradle 文件中

plugins {
    //生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码
    id("androidx.navigation.safeargs.kotlin")
    //生成仅适用于 Kotlin 模块的 Kotlin 语言代码
    id("androidx.navigation.safeargs.kotlin")
}

配置完成以后记得要clean rebuild 一下,会生成对应的Directions类。


image.png

启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。
1.为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 SpecifyAmountFragment 的 Fragment,则生成的类的名称为 SpecifyAmountFragmentDirections。
2.该类会为源目的地中定义的每个操作提供一个方法。
对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 confirmationAction,,则类名称为 ConfirmationAction。如果您的操作包含不带 defaultValue 的参数,则您可以使用关联的 action 类来设置参数值。
3.为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 ConfirmationFragment,,则生成的类的名称为 ConfirmationFragmentArgs。可以使用该类的 fromBundle() 方法检索参数。

2.无参跳转
btn_second.setOnClickListener {
  val action = HomeFragmentDirections.actionHomeFragmentToSecondFragment()
  findNavController().navigate(action)
}
3.有参跳转
第一步:创建参数

Design 模式下,点击启示位置(注意是启示位置)布局信息,看到他变成蓝色以后。看到右边面板上有 Arguments


image.png

创建了两个字段。name age 对应的导航图中也可以看到其信息。

<fragment
        android:id="@+id/homeFragment"
        android:name="com.alan.mynavigation.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
        <argument
            android:name="name"
            app:argType="string"
            android:defaultValue="alan" />
        <argument
            android:name="age"
            app:argType="integer"
            android:defaultValue="18" />
    </fragment>
argument标签
字段 含义
android:name 参数名称
android:defaultValue 为默认值
app:argType 为参数类型
app:nullable 为标记是否可空
第二步:生成Args类

创建完成clean rebuild 一下,插件会给我们生成一个 Args结尾的类。


image.png
第三步:带参数跳转
btn_second.setOnClickListener {
  val bundle = HomeFragmentArgs("张三",20).toBundle()
  findNavController().navigate(R.id.action_homeFragment_to_secondFragment,bundle)
}
第四步:接收参数
//方式1:使用 fromBundle
arguments?.apply {
  val fragmentArgs = HomeFragmentArgs.fromBundle(this)
  val username = fragmentArgs.name
  val age = fragmentArgs.age
  tv_info.text = "$username $age"
}


//方式2: 使用委托的方式
val args by navArgs<HomeFragmentArgs>()

 tv_info.text = "$username $age"
4.使用Action方式有参跳转

从上个列子可以看出虽然携带参数跳转了,但使用的是id

findNavController().navigate(R.id.action_homeFragment_to_secondFragment,bundle)

插件为我们生成的HomeFragmentDirections不可以携带参数,这是因为需要跳转双方都声明相同的参数。然后clean rebuild一下生成新的带参数的HomeFragmentDirections。


image.png
btn_second.setOnClickListener {
    val action = HomeFragmentDirections.actionHomeFragmentToSecondFragment("张三", 20)
    findNavController().navigate(action)
}

7、Deep Links

深层链接是指将用户直接转到应用内特定目的地的链接。
比如点击一下通知就要跳转到对应的消息详情界面。

1.显式深层链接
btn_second.setOnClickListener {
    val bundle = Bundle().apply {
        putString("username", "alan")
        putInt("age", 18)
    }


    val pendingIntent = NavDeepLinkBuilder(requireActivity())
        .setGraph(R.navigation.nav_graph)
        .setDestination(R.id.secondFragment)
        .setArguments(bundle)
        //默认情况下,NavDeepLinkBuilder 会将显式深层链接启动到应用清单中声明的默认启动 Activity。如果您的 NavHost 在其他 activity 中,则您必须在创建深层链接建立工具时指定其组件名称:
        .setComponentName(DetailActivity::class.java)
        .createPendingIntent()


    //通知
    val notificationManager = requireActivity().getSystemService(NOTIFICATION_SERVICE) as (NotificationManager)

    val CHANNEL_ID = "1000"
    val channel = NotificationChannel(
        CHANNEL_ID, "测试",
        NotificationManager.IMPORTANCE_DEFAULT
    )
    notificationManager.createNotificationChannel(channel);

    val builder = NotificationCompat.Builder(requireActivity(), CHANNEL_ID)
        .setContentTitle("测试")
        .setContentText("内容")
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
        .setSmallIcon(R.drawable.ic_launcher_foreground)
        .setContentIntent(pendingIntent)
        .setAutoCancel(true)
    notificationManager.notify(1, builder.build())
}
2.创建隐式深层链接
第一步:创建隐式深层链接

可以通过 URI、intent 操作和 MIME 类型匹配深层链接。您可以为单个深层链接指定多个匹配类型,但请注意,匹配的优先顺序依次是 URI 参数、操作和 MIME 类型。例如:

<deeplink app:uri="www.example.com"
                app:action="android.intent.action.MY_ACTION"
                app:mimeType="type/subtype"/>
image.png

Add Deep Link 对话框中请注意以下几点:

  • 没有架构的 URI 会被假定为 http 或 https。例如,www.google.com 同时与 http://www.google.comhttps://www.google.com 匹配。
  • 形式为 {placeholder_name} 的路径参数占位符与一个或多个字符相匹配。例如,http://www.example.com/users/{id}http://www.example.com/users/4 匹配。Navigation 组件通过将占位符名称与已定义的参数(为深层链接目的地所定义)相匹配,尝试将占位符值解析为相应的类型。如果没有定义具有相同名称的参数,对参数值使用默认的 String 类型。您可以使用 .* 通配符匹配 0 个或多个字符。
  • 可以使用查询参数占位符代替路径参数,也可以将查询参数占位符与路径参数结合使用。例如,http://www.example.com/users/{id}?myarg={myarg}http://www.example.com/users/4?myarg=28 匹配。
  • 使用默认值或可为 null 的值所定义的变量的查询参数占位符无需匹配。例如,http://www.example.com/users/{id}?arg1={arg1}&arg2={arg2}http://www.example.com/users/4?arg2=28http://www.example.com/users/4?arg1=7 匹配。不过,路径参数并非如此。例如,http://www.example.com/users?arg1=7&arg2=28 就与上述模式不匹配,因为未提供所需的路径参数。
  • 多余的查询参数不会影响深层链接 URI 匹配。例如,即使 URI 模式中未定义 extraneousParamhttp://www.example.com/users/{id} 也与 http://www.example.com/users/4?extraneousParam=7 匹配。
  • (可选)选中 Auto Verify 可要求 Google 验证您是相应 URI 的所有者。
<fragment
        android:id="@+id/secondFragment"
        android:name="com.alan.mynavigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second">
        <argument
            android:name="name"
            android:defaultValue="alan"
            app:argType="string" />
        <argument
            android:name="age"
            android:defaultValue="18"
            app:argType="integer" />
        <deepLink
            android:id="@+id/deepLink"
            app:uri="http://www.alan.com/second/${name}" />
    </fragment>
第二步:添加nav-graph 声明

应用的manifest.xml 文件中,将一个 <nav-graph> 元素添加到指向现有导航图的 Activity标签中。

<activity
  android:name=".MainActivity"
  android:exported="true">
  <intent-filter>
      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
  <nav-graph android:value="@navigation/nav_graph" />
</activity>

构建项目时,Navigation 组件会将 <nav-graph> 元素替换为生成的 <intent-filter> 元素,以匹配导航图中的所有深层链接。

在触发隐式深层链接时,返回堆栈的状态取决于是否使用Intent.FLAG_ACTIVITY_NEW_TASK标志启动隐式 Intent:

  • 如果该标志已设置,任务返回堆栈就会被清除,并被替换为相应的深层链接目的地。与显式深层链接一样,当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个 <navigation> 元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,他们会返回到相应的导航堆栈,就像从入口点进入您的应用一样。
  • 如果该标记未设置,您仍会位于触发隐式深层链接时所在的上一个应用的任务堆栈中。在这种情况下,如果按下返回按钮,您会返回到上一个应用;如果按下向上按钮,就会在导航图中的父级目的地上启动应用的任务。

使用 Navigation 时,强烈建议您始终使用默认launchMode:standard。使用standard启动模式时,Navigation 会调用handleDeepLink()来处理Intent中的任何显式或隐式深层链接,从而自动处理深层链接。但是,如果在使用备用 singleTop 等备选 launchMode 时重复使用了相应 Activity,则这不会自动发生。在这种情况下,有必要在 onNewIntent() 中手动调用 handleDeepLink(),如以下示例所示:

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    navController.handleDeepLink(intent)
}
第三步:使用隐式深层链接
btn_second.setOnClickListener {
    val name = "李四"
    findNavController().navigate("http://www.alan.com/second/${name}".toUri())
}

8、添加动画效果

<?xml version="1.0" encoding="utf-8"?>
<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.alan.mynavigation.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
        <argument
            android:name="name"
            app:argType="string"
            android:defaultValue="alan" />
        <argument
            android:name="age"
            app:argType="integer"
            android:defaultValue="18" />
    </fragment>

    <fragment
        android:id="@+id/secondFragment"
        android:name="com.alan.mynavigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second">
        <argument
            android:name="name"
            android:defaultValue="alan"
            app:argType="string" />
        <argument
            android:name="age"
            android:defaultValue="18"
            app:argType="integer" />
        <deepLink
            android:id="@+id/deepLink"
            app:uri="http://www.alan.com/second/{name}" />
    </fragment>


</navigation>

参考:Android 开发者网站

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容