Jetpack-关于Navigation的使用(Kotlin)

封面

参考:Codelabs-Jetpack Navigation

参考:官方文档

如有错误欢迎指出

我对Navigation的理解

  • 对于Fragment:不用去操作supportFragmentManager,也就意味着对replaceshowhide说拜拜了。
  • 对于Activity:可能会大幅减少Activity,减少使用startActivity因为fragment有更详细的生命周期,更方便的传参。
  • 使用deepLink深链跳转更加方便。

三个关键组成部分

Navigation graph

一个xml资源文件,文件目录为res/navigation/*.xml,配置关于Navigation的导航内容。也可以理解成路由配置。

资源属性

<?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/sample_navigation"
    app:startDestination="@id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        android:name="com.cxl.jetpack.nav.HomeFragment"
        tools:layout="@layout/home_dest"
        android:label="HomeFragment" >
        <action
            android:id="@+id/open_one_action"
            app:destination="@id/one_dest"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            />
    </fragment>
    <fragment
        android:id="@+id/one_dest"
        android:name="com.cxl.jetpack.nav.OneFragment"
        android:label="OneFragment" >
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>
    </fragment>
</navigation>
  • tools:layout:预览属性,如果不配置该属性在Design面板会看不见预览。
  • action:字面理解就是动作,作用是fragment之间进行切换。
  • destination:目的地,跳转的目的地。
  • enterAnimexitAnimpopEnterAnimpopExitAnim:是页面切换和弹窗动画
  • argument:类似于Activity的跳转传参,只不过传参取参更加方便简单,如下:
    //传值且跳转
    val flowStepNumberArg=1
    val action = HomeFragmentDirections.nextAction(flowStepNumberArg)
    findNavController().navigate(action)
    //取值
    val safeArgs: FlowStepFragmentArgs by navArgs()
    val flowStepNumber = safeArgs.flowStepNumber
    

也可以使用Design面板(可以参考这篇文章:https://developer.android.com/guide/navigation/navigation-getting-started#nav-editor)进行配置,更方便。

NavHost

理解为主机,作用在fragment布局之上,如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
       ... />

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/sample_navigation" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        ... />

</LinearLayout>

可以看到比一般的fragment布局多了三个属性android:name="androidx.navigation.fragment.NavHostFragment"app:defaultNavHost="true"app:navGraph="@navigation/sample_navigation"

  • android:name:指向NavHost的实现类NavHostFragment
  • app:defaultNavHost:默认值为false,当该值为false的时候,当前Activity中的Fragment使用了Navigation,且使用Navigation跳转到下一个Fragment,在下一个Fragment页面中点击了Back键会退出当前Activity。为true的时候是回到上一个Fragment中,如果上一个Fragmentnull才会退出当前Activity。类似于我们处理WebView的back事件。
  • app:navGraph:指向Navigation graph配置文件

NavController

可以理解为路由跳转控制器,在NavHost中协调目标内容的交换。如下:

//无参跳转
findNavController().navigate(R.id.flow_step_one_dest, null, options)
//或者携带参数
val flowStepNumberArg=1
val action = HomeFragmentDirections.nextAction(flowStepNumberArg)
findNavController().navigate(action)

那么该如何使用

第一步导入依赖

在App目录下的build.gradle内加入以下代码:

...
dependencies{
    ...
    //Navigation
    implementation "androidx.navigation:navigation-fragment-ktx:2.1.0"
    implementation "androidx.navigation:navigation-ui-ktx:2.1.0"
}

最好是使用最新版本,最新版本可以在这里查阅(打不开建议检查网络):Google's Maven Repository

可选*:项目级的build.gradle内加入以下代码:(加入这个Fragment质检传递参数更加方便)

···
 dependencies {
        ...
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
    }

版本号都是一致的,推荐加个变量统一管理,比如在项目级build.gradle

buildscript{
   ext{
        navigationVersion = "2.0.0"
   } 
}

以上依赖可以分别修改为:

//项目级的dependencies
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
//App级的dependencies
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"

第二步基本框架搭建

新建几个Fragment并实现布局比如:

deep_link_dest.xml(对应DeepLinkFragment)

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    android:textSize="80sp"
    android:text="Deep Link"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="HardcodedText"/>

flow_step_one_dest.xml(对应OneFragment)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    tools:ignore="HardcodedText"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:textSize="68sp"
        android:text="1"
        android:layout_marginBottom="50dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:text="open 2"
        android:layout_width="200dp"
        android:layout_height="wrap_content"/>

</LinearLayout>

flow_step_two_dest.xml(对应TwoFragment)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    tools:ignore="HardcodedText"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/twoTxt"
        android:textSize="68sp"
        android:text="2"
        android:layout_marginBottom="50dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:text="Finish Flow"
        android:layout_width="200dp"
        android:layout_height="wrap_content"/>

</LinearLayout>

home_dest.xml(对应HomeFragment)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    tools:context="com.cxl.jetpack.nav.HomeFragment"
    tools:ignore="HardcodedText"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:textSize="68sp"
        android:text="HOME"
        android:layout_marginBottom="50dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/open1"
        android:text="open 1"
        android:layout_width="200dp"
        android:layout_height="wrap_content"/>

</LinearLayout>

新建Navigation graph资源文件

  • 单击res文件夹,右键菜单选择New->Android Resource File
  • 输入File name,选择Resource type为Navigation
  • 点击ok即可创建Navigation graph资源文件
    把新建的Fragment引入到该资源文件内
image

比如在这里点击TwoFragment就可以把TwoFragment引入到资源内

新建menu/bottom_nav_menu.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@id/home_dest"
        android:icon="@drawable/ic_home"
        android:title="home" />
    <item
        android:id="@id/deep_link_dest"
        android:icon="@drawable/ic_android"
        android:title="dest" />
</menu>

这里的item.id和Navigation graph资源文件中的fragment.id相对应,目前Navigation graph内容如下:

<?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/sample_navigation"
    app:startDestination="@id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        ... >
        ...
    </fragment>
    <fragment
        android:id="@+id/one_dest"
        ... >
        ...
    </fragment>
    <fragment
        android:id="@+id/deep_link_dest"
        ... />
</navigation>

最后新建MainActivity和activity_main.xml。
activity_main.xml内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/sample_navigation" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:menu="@menu/bottom_nav_menu" />

</LinearLayout>

MainActivity内如如下:

package com.cxl.jetpack.nav

import android.content.res.Resources
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //第一步获取到NavHostFragment
        val host: NavHostFragment = supportFragmentManager
            .findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment? ?: return
        //第二步获取到NavController
        val navController = host.navController
        //第三步配置BottomNavigationView
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
        bottomNav?.setupWithNavController(navController)
        //第四步添加路由监听
        navController.addOnDestinationChangedListener { _, destination, _ ->
            val dest: String = try {
                resources.getResourceName(destination.id)
            } catch (e: Resources.NotFoundException) {
                destination.id.toString()
            }
            Toast.makeText(this@MainActivity, "Navigated to $dest",
                Toast.LENGTH_SHORT).show()
            Log.d("NavigationActivity", "Navigated to $dest")
        }
    }
}

这样基本的框架就搭好啦。

home dest
image
image

路由跳转

无参跳转

在HomeFragment中的onViewCreated(view: View, savedInstanceState: Bundle?)函数中添加以下代码:

view.findViewById<Button>(R.id.open1).setOnClickListener(Navigation.createNavigateOnClickListener(R.id.open_one_action))

其中R.id.open_one_action是我们在Navigation graph资源内添加的action属性,如下:

<?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/sample_navigation"
    app:startDestination="@id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        android:name="com.cxl.jetpack.nav.HomeFragment"
        tools:layout="@layout/home_dest"
        android:label="HomeFragment" >
        <action
            android:id="@+id/open_one_action"
            app:destination="@id/one_dest"
            ...
            />
    </fragment>
    <fragment
        android:id="@+id/one_dest"
        tools:layout="@layout/flow_step_one_dest"
        android:name="com.cxl.jetpack.nav.OneFragment"
        android:label="OneFragment" >
        <action
            android:id="@+id/action_one_dest_to_twoFragment"
            app:destination="@+id/tow_dest"
            ... />
    </fragment>
    <fragment
        tools:layout="@layout/deep_link_dest"
        android:id="@+id/deep_link_dest"
        android:name="com.cxl.jetpack.nav.DeepLinkFragment"
        android:label="DeepLinkFragment" />
    <fragment
        tools:layout="@layout/flow_step_two_dest"
        android:id="@+id/tow_dest"
        android:name="com.cxl.jetpack.nav.TwoFragment"
        android:label="tow_dest" />
</navigation>

有参跳转

我们现在要往TwoFragment内跳转,且携带一个名为text的参数,该参数的值回覆盖id为twoTxt的TextView的text值。

第一步修改Navigation graph文件

Navigation graphid为tow_dest的fragment节点添加子节点argument,如下所示

<?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/sample_navigation"
    app:startDestination="@id/home_dest">
    ...
    <fragment
        tools:layout="@layout/flow_step_two_dest"
        android:id="@+id/tow_dest"
        android:name="com.cxl.jetpack.nav.TwoFragment"
        android:label="tow_dest" >
        <argument
            android:name="text"
            app:argType="string"
            android:defaultValue="2"/>
    </fragment>
</navigation>

添加完后,此时我们build一下代码,在build\generated\source\navigation-args\debug\com\cxl\jetpack\nav目录下会生成一个TwoFragmentArgs类,我用该类进行传参跳转。

快速查看生成的类

OneFragment中的onViewCreated(view: View, savedInstanceState: Bundle?)函数添加如下代码:

 view.findViewById<Button>(R.id.open2).setOnClickListener {
    findNavController().navigate(OneFragmentDirections.actionOneDestToTwoFragment("Hi"))
}

接收参数

val arg : TwoFragmentArgs by navArgs()
view.findViewById<TextView>(R.id.twoTxt).text=arg.text

记住要用Java8

回到首页

接下来我们从TwoFragment回到HomeFragment

配置action

<?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/sample_navigation"
    app:startDestination="@id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        ... />
    ...
    <fragment
        android:id="@+id/tow_dest"
        ... >
        ...
        <action
            android:id="@+id/action_tow_dest_to_home_dest"
            app:popUpTo="@id/home_dest" />
    </fragment>
</navigation>

注意这里用的是app:popUpTo而不是destination,关于app:popUpTo可以参考这个:popUpTo and popUpToInclusive

使用Navigation返回首页

TwoFragmentoverride fun onViewCreated(view: View, savedInstanceState: Bundle?)函数中添加如下代码:

//取值
val arg : TwoFragmentArgs by navArgs()
view.findViewById<TextView>(R.id.twoTxt).text=arg.text
//点击按钮跳转
view.findViewById<Button>(R.id.flow)
    .setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_tow_dest_to_home_dest))

使用Deep Link进行跳转

Deep Link一般的使用场景,app推送打开指定页面。

添加Deep Link

如下代码:

<?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/sample_navigation"
    app:startDestination="@id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        .../>
    <fragment
        android:id="@+id/deep_link_dest"
        android:name="com.cxl.jetpack.nav.DeepLinkFragment"
        android:label="DeepLinkFragment"
        tools:layout="@layout/deep_link_dest">
        <argument
            android:name="dlValue"
            android:defaultValue="Deep Link"
            app:argType="string" />
        <deepLink
            android:id="@+id/deepLink"
            app:uri="https://cxl.cn/sample/nav/open-deep-link/{dlValue}" />
    </fragment>
    ...
</navigation>

随后在Manifests.xml添加nav-graph,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">
    <application ... >
        <activity name=".MainActivity" ...>
            ...
            <nav-graph android:value="@navigation/sample_navigation" />
            ...
        </activity>
    </application>
</manifest>

使用Deep Link

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val args : DeepLinkFragmentArgs by navArgs()
        view.findViewById<TextView>(R.id.dlTv).run {
            text = args.dlValue
            setOnClickListener {
                val bundle = Bundle()
                bundle.putString("dlValue","通知跳转")
                val deepLink = findNavController()
                    .createDeepLink()
                    .setArguments(bundle)
                    .setDestination(R.id.deep_link_dest)
                    .createPendingIntent()
                val notificationManager =
                    context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    notificationManager.createNotificationChannel(NotificationChannel(
                        "Deep Link", "Deep Links", NotificationManager.IMPORTANCE_HIGH)
                    )
                }
                val builder = NotificationCompat.Builder(
                    context!!, "Deep Link")
                    .setContentTitle("通知")
                    .setContentText("跳转到Deep Link")
                    .setSmallIcon(R.drawable.ic_android)
                    .setContentIntent(deepLink)
                    .setAutoCancel(true)
                notificationManager.notify(0, builder.build())
            }
        }
    }

以上代码实现点击TextView发送一个通知,用户点击通知跳转到Deep Link指向页面。

源码地址:GitHub

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

推荐阅读更多精彩内容