最全面的Navigation的使用指南

斜博超
2023-12-01

Navigation可以目前看做Google对于之前的Fragment的不满, 重新搭建的一套Fragment管理框架. 但是Navgation未来应该不仅限于Fragment导航.

并且Navigation可以和BottomNavigationView/NavigationView/Toolbar等结合使用, 不再需要去写冗余代码管理Fragment.

并且具备完善的Fragment回退栈管理.

如果是使用Java语言我不推荐使用任何新框架了, 就自己玩自己的吧.

ktx (除基本依赖外还包含一些Kotlin新特性的函数)

implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
复制代码

一些常用的关键字解释

navigation(nv): 导航, 即Navigation框架的fragment返回栈

graph: 指一个描述返回栈关系的xml文件 (NavigationResourceFile) 也是布局编辑器用于显示图表的界面数据来源

destination: 目标, 即在返回栈中要跳转的新页面

pop: 弹出栈, 会弹出所有不符合目标的页面, 直至找到目标页面(默认情况不弹出目标页面也可以设置), 可以理解为Fragment的singleTask模式

navHost: 即所有页面的容器. 类似网页中的host, 所有path路径都是在host之后跟随, host固定不变.

XML编辑

点击NavResourceFile中的Design即可查看布局编辑器, 布局编辑器分为三栏.

左侧是已添加的导航, 中间是页面浏览, 中间栏的工具栏可以创建和快速添加标签以及整理页面, 右侧属性栏方便添加属性.

navigation这是个嵌套的图表, 可以点击打开新的图表页面.

Activity布局中

<LinearLayout
    .../>
    <androidx.appcompat.widget.Toolbar
        .../>
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/mobile_navigation"
        app:defaultNavHost="true"
        />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        .../>
</LinearLayout>
复制代码
android:name="androidx.navigation.fragment.NavHostFragment"  
固定写法

app:navGraph 
指定navigation资源文件, 也可以不指定后面通过代码中动态设置

app:defaultNavHost 
是否拦截返回键事件, false表示不需要回退栈.
复制代码

NavigationResourceFile

res目录创建 AndroidResourceFile 选择 Navigation. 然后 new-> NavigationResourceFile

navigation

app:startDestination="@+id/home_dest" 指定初始目标
复制代码

navigation可以嵌套navigation标签.

在布局编辑器中会显示为

嵌套navigation无法互相关联

<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_global"
    app:startDestination="@id/mainFragment">

 <!--   <action
        android:id="@+id/global_action"
        app:destination="@id/navigation" />-->

    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.frameexample.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main">
        <action
            android:id="@+id/action_mainFragment_to_personInfoFragment"
            app:destination="@id/settingFragment" />
    </fragment>

    <navigation
        android:id="@+id/navigation"
        app:startDestination="@id/settingFragment">
        <fragment
            android:id="@+id/settingFragment"
            android:name="com.example.frameexample.SettingFragment"
            android:label="fragment_setting"
            tools:layout="@layout/fragment_setting" />
    </navigation>

</navigation>
复制代码

上面的mainFragment无法直接app:destination="@id/settingFragment"这会导致运行错误. 只能先导航到navigation.(即NavHostFragment所在的界面)

fragment

android:id 不言而喻

android:name 目标要实例化的fragment完全限定类名
 
tools:layout 用于显示在布局编辑器

android:label  用于后面绑定Toolbar等自动更新标题
复制代码

argument

android:name="myArg"
app:argType="integer"
android:defaultValue="0"
复制代码
  • 参数名称
  • 参数类型
  • 参数默认值

在跳转导航页面的时候会自动在argument中带上参数(要求指定参数默认值). 数组和Paraclable/Serializable不支持默认值设置, 通过下面要讲的SafeArg可以在编译器校验参数类型安全问题.

action

动作 用于页面跳转时指定目标页面

android:id="@+id/next_action" 
动作id
app:destination="@+id/flow_step_two_dest"> 
目标页面
app:popUpTo="@id/home_dest" 
当前属于弹出栈
app:popUpToInclusive="true/false" 
弹出栈是否包含目标
app:launchSingleTop="true/false" 
是否开启singleTop模式

app:enterAnim=""
app:exitAnim=""
导航动画

app:popEnterAnim=""
app:popExitAnim=""
弹出栈动画
复制代码

如果从导航页面到新的Activity页面, 动画不支持. 请使用默认的Activity设置动画去支持.

全局动作

一般情况下NavController只能使用当前Fragment在NavXML中声明的子标签action, 但是可以通过直接给navigation标签创建子标签action实现全局动作, 即每个Fragment都能使用的动作.

给navigation添加action子标签时要求给navigation指定熟悉android:id

占位页面如果运行时没有指定Class并且导航到该占位页面时会抛出异常

类关系

涉及到的类关系

  • NavController 控制导航的跳转和弹出栈
  • NavOptions 控制跳转过程中的配置选项, 例如动画和singleTop模式
  • Navigation 工具类 创建点击事件或者获取控制器
  • NavHostFragment 导航的容器, 可以设置和获取导航图(NavGraph)
  • NavGraph 用于描述导航中页面关系的对象 可以增删改查页面,设置起始页等
  • NavigationUI 用于将导航和一系列菜单控件自动绑定的工具类
  • Navigator 页面的根接口, 如果想创建一个新的类型页面就要自定义他
  • NavDeepLinkBuilder 构建一个能打开导航页面的Intent

NavController

NavController用于跳转页面和参数传递等控制, 可以通过扩展函数得到实例.

Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)
复制代码

导航

public final void navigate (int resId)

public final void navigate (int resId, 
                Bundle args)

public void navigate (int resId, 
                Bundle args, 
                NavOptions navOptions)

public void navigate (NavDirections directions)

public void navigate (NavDirections directions, 
                NavOptions navOptions)
  
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
                       @Nullable Navigator.Extras navigatorExtras)

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
                        @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras)

public boolean navigateUp ()
返回到上一个页面

public boolean navigateUp (DrawerLayout drawerLayout)

public boolean navigateUp (AppConfiguration appConfiguration)
复制代码

resId 可以是NavXML中的action或者destination标签id, 如果是action则会附带action的选项, 如果是页面destination则不会附带destination标签下的子标签action(写了白写).

args 即需要在fragment之间传递的Bundle参数, 但是导航还支持另外一种插件形式的传递参数方式-安全参数SafeArgs, 后面提到.

navOptions 即导航页面一些配置选项(例如动画)

navigatorExtras 目前是用于支持转场动画的共享元素.

ActivityNavigator和FragmentNavigator内部都实现了Navigator.Extras

通过扩展函数可以快速创建

fun FragmentNavigatorExtras(vararg sharedElements: Pair<View, String>) =
        FragmentNavigator.Extras.Builder().apply {
            sharedElements.forEach { (view, name) ->
                addSharedElement(view, name)
            }
        }.build()

fun ActivityNavigatorExtras(activityOptions: ActivityOptionsCompat? = null, flags: Int = 0) =
        ActivityNavigator.Extras.Builder().apply {
            if (activityOptions != null) {
                setActivityOptions(activityOptions)
            }
            addFlags(flags)
        }.build()

复制代码

可以从源码看到内部都是使用的Extras.Builder构造器创建的.

示例

    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_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>
复制代码

上面你给action指定动画, 但是如果你使用的navigate中的参数resId不是R.id.next_action而是R.id.home_dest. 那么你这个action相当于不生效.

弹出栈, 即从Nav回退栈中清除Fragment.

public boolean popBackStack (int destinationId,  // 目标id
                boolean inclusive) // 是否包含参数目标
  
public boolean popBackStack ()
弹出当前Fragment
复制代码

监听导航

public void addOnDestinationChangedListener (NavController.OnDestinationChangedListener listener)
public void removeOnDestinationChangedListener (NavController.OnDestinationChangedListener listener)
复制代码

回调

    public interface OnDestinationChangedListener {
        /**
         * 导航完成以后回调函数(但是可能动画还在播放中)
         *
         * @param 控制导航到目标的导航控制器NavController
         * @param 目标页面
         * @param 导航到目标页面的参数
         */
        void onDestinationChanged(@NonNull NavController controller,
                @NonNull NavDestination destination, @Nullable Bundle arguments);
    }
复制代码

状态保存和恢复

public Bundle saveState ()
public void restoreState (Bundle navState)
复制代码
public NavDestination getCurrentDestination ()

public NavGraph getGraph ()
public void setGraph (int graphResId)
public void setGraph (NavGraph graph)

public NavigatorProvider getNavigatorProvider ()

public NavDeepLinkBuilder createDeepLink ()
创建一个打开当前页面的深层链接构造器
复制代码

NavigatorProvider是一个提供者, 可以添加和查询Navigator.

NavOptions

属于导航时的附加选项设置

相当于代码动态实现了NavigationResourceFile中的<action>标签的属性设置(但是无法指定目标).

目前功能只有设置动画和singleTop(启动模式), popUp(弹出栈)

提供一个DSL作用域

fun navOptions(optionsBuilder: NavOptionsBuilder.() -> Unit): NavOptions
复制代码

示例

val options = navOptions {
  anim {
    enter = R.anim.slide_in_right // 进入页面动画
    exit = R.anim.slide_out_left
    popEnter = R.anim.slide_in_left  // 弹出栈动画
    popExit = R.anim.slide_out_right
  }

  launchSingleTop = true
  popUpTo = R.id.categoryFragment
}

findNavController().navigate(R.id.flow_step_one_dest, null, options)
复制代码

如果是跳转到新的Activity当前设置的动画都不支持

弹出栈

public NavOptions.Builder setPopUpTo (int destinationId, 
                boolean inclusive)
复制代码

NavOptions这个函数和NavController有所区别, 他并不会决定目的地, 只是在导航到目的地之前先执行一个弹出栈指令.

场景: 例如我现在购买一个商品支付成功, 这个时候我要将前面的商品详情; 订单配置等页面全部关闭 然后进入<支付成功>页面

顺便说下在之前的做法是发送事件然后finish

Navigation

工具类

目前只支持创建点击事件和获取控制器

public static View.OnClickListener createNavigateOnClickListener (int resId)
public static View.OnClickListener createNavigateOnClickListener (int resId, 
                Bundle args)
快速创建一个跳转到目标的View.OnClickListenner


public static NavController findNavController (Activity activity, 
                int viewId)                
public static NavController findNavController (View view)
public static void setViewNavController (View view, 
                NavController controller)
以上都可以使用Kotlin扩展函数获取
复制代码

NavHostFragment

该对象为Navigation提供一个容器

一般使用情况是在布局中直接定义, 但是也可以通过代码构建实例, 然后通过代码创建视图(例如ViewPager等)

public static NavHostFragment create (int graphResId)
复制代码
public static NavController findNavController (Fragment fragment)
这个函数实际上就是findNavController()扩展函数的实际执行函数
复制代码

NavigationUI

该工具类负责绑定视图控件和导航, 所有绑定都只需要id对应即可自动导航.

设置导航到新页面时自动更新标题文字

fun AppCompatActivity.setupActionBarWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) 

fun AppCompatActivity.setupActionBarWithNavController(
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)
复制代码

这里出现个参数AppBarConfiguration, 用于配置Toolbar/ActionBar/CollapsingToolbarLayout.

构造器模式使用Builder创建实例

AppBarConfiguration.Builder(NavGraph navGraph)
顶级目标是NavGraph的起始页面

AppBarConfiguration.Builder(Menu topLevelMenu)
菜单包含的全部是顶级目标

AppBarConfiguration.Builder(int... topLevelDestinationIds)
顶级目标集合
AppBarConfiguration.Builder(Set<Integer> topLevelDestinationIds)
复制代码

顶级目标: 顶级目标即表示为回退栈最底位置, 无法再返回, 故可以理解为不需要返回键导航的页面(Toolbar等就不会显示返回箭头).

函数

AppBarConfiguration.Builder setDrawerLayout(DrawerLayout drawerLayout)
绑定Toolbar同时绑定一个DrawerLayout联动

AppBarConfiguration.Builder setFallbackOnNavigateUpListener(AppBarConfiguration.OnNavigateUpListener fallbackOnNavigateUpListener)

AppBarConfiguration  build()
复制代码

AppBarConfiguration.OnNavigateUpListener 该回调接口会在每次点击向上导航时回调

public interface OnNavigateUpListener {
/**
* 回调处理向上导航
*
* @return 返回true表示向上导航, false不处理
*/
boolean onNavigateUp();
}
复制代码

Toolbar也可以绑定Nav自动更新对应页面的标题

fun Toolbar.setupWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) {
    NavigationUI.setupWithNavController(this, navController,
        AppBarConfiguration(navController.graph, drawerLayout))
}

fun Toolbar.setupWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) {
    NavigationUI.setupWithNavController(this, navController,
        AppBarConfiguration(navController.graph, drawerLayout))
}
复制代码

CollapsingToolbarLayout

fun CollapsingToolbarLayout.setupWithNavController(
    toolbar: Toolbar,
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)

fun CollapsingToolbarLayout.setupWithNavController(
    toolbar: Toolbar,
    navController: NavController,
    drawerLayout: DrawerLayout?
)
复制代码

绑定菜单条目点击自动导航

fun MenuItem.onNavDestinat ionSelected(navController: NavController): Boolean =
        NavigationUI.onNavDestinationSelected(this, navController)
复制代码

在onOptionsItemSelected

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return item.onNavDestinationSelected(findNavController(R.id.my_nav_host_fragment))
    }
复制代码

Nav绑定NavigationView菜单



fun NavigationView.setupWithNavController(navController: NavController) {
    NavigationUI.setupWithNavController(this, navController)
}

fun BottomNavigationView.setupWithNavController(navController: NavController) {
    NavigationUI.setupWithNavController(this, navController)
}
复制代码

DeepLink

Nav声明一个DeepLink(深层链接)只需要给Fragment添加一个子标签即可

首先要在AndroidManifest中的activity中添加一个子标签nav-graph为NavRes注册DeepLink.

<activity>
	<nav-graph android:value="@navigation/mobile_navigation" />
</activity>
复制代码

深层链接

<deepLink app:uri="www.example.com/{myarg}" />
复制代码

通过ADB测试

adb shell am start -a android.intent.action.VIEW -d "http://www.example.com/2334456"
复制代码

2334456即传递过去的参数

{}包裹的字段属于变量, *可以匹配任意字符

通过NavDeepLinkBuilder创建DeepLink Intent

NavDeepLinkBuilder	setArguments(Bundle args)

NavDeepLinkBuilder	setDestination(int destId)

NavDeepLinkBuilder	setGraph(int navGraphId)

NavDeepLinkBuilder	setGraph(NavGraph navGraph)
复制代码

生成PendingIntent可以用于开启界面(例如传给Notification)

PendingIntent	createPendingIntent()

TaskStackBuilder	createTaskStackBuilder()
复制代码

SafeArgs

安全类型插件, 基于Gradle实现的插件.

他的目的就是根据你在NavRes中声明argument标签生成工具类, 然后全部使用工具类而不是字符串去获取和设置参数. 避免前后两者参数类型不一致而崩溃.

插件

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
    }
}
复制代码

应用插件

apply plugin: 'androidx.navigation.safeargs'
复制代码

在NavigationResourceFile中声明<argument>标签后会自动生成类

插件会根据页面自动生成*Directions类, 该类包含该页面能使用的所有跳转动作(包含全局动作和自身动作).

生成类会包含一个有规则的静态函数用于获取Directions的实现类(*Directions的静态内部类), 函数名称规则为

action<页面名称>To<目标页面名称>

全局动作名称规则则为: 动作id的变量命名法

例: public static ActionMainFragmentToPersonInfoFragment actionMainFragmentToPersonInfoFragment()
复制代码

这里看下NavDirections接口的含义

public interface NavDirections {

    /**
     * 返回动作id
     *
     * @return id of an action
     */
    @IdRes
    int getActionId();

    /**
     * 返回目标参数
     */
    @NonNull
    Bundle getArguments();
}
复制代码

可以总结为 包含携带参数和动作.

但是如果navRes中还包含<argument>标签, 则还会生成对应的*Args类, 并且上面提到的自动生成的*Directions中的NavDirection静态内部类还会生成参数的构造和访问器

还有一系列 hashCode/equals/toString 函数

完整的导航页面且传递数据写法

导航至目标页面

val action =
MainFragmentDirections.actionMainFragmentToPersonInfoFragment().setName("设计师吴彦祖")

findNavController().navigate(action)
复制代码

在目标页面接受数据

tv.text = PersonInfoFragmentArgs.fromBundle(arguments!!).name
复制代码

这里可以再次想下什么是安全类型参数.

总结

关于Google推动的SingleActivity构建应用我说下我的看法, 我认为整个应用使用一个Activity还是比较麻烦的.

列举下所谓麻烦

  1. Fragment无法设置默认动画, 动画统一管理起来很麻烦
  2. 所谓Fragment减少内存开销用户都无法感知
  3. 很多框架还是基于Activity实现的(例如路由,状态栏), 可能某些项目架构会受到局限

我认为Navigation替代FragmentManger还是得心应手的, 并且导航图看起来也很有逻辑感.

转载于:https://juejin.im/post/5ccd38fd6fb9a031f0380b2b

 类似资料: