Android&Navigation全面介绍&全新的Fragment管理器

11,096 阅读12分钟

Navigation属于具备比较完善回退栈的Fragment导航器. 并且Navigation可以和BottomNavigationView/NavigationView/Toolbar等结合使用, 内部使用的是Fragment的replace而非show|hidden, 所以不存在穿透现象. 生命周期存在detachView, 故视图需要自己复用. 虽然Fragment的使用变得比较清晰和方便了, 但是我依旧不推荐使用Fragment去替代Activity. 那只是徒增麻烦而已.

我平时项目开发必备框架

  1. Android上最强网络请求 Net
  2. Android上最强列表(包含StateLayout) BRV
  3. Android最强缺省页 StateLayout
  4. JSON和长文本日志打印工具 LogCat
  5. 支持异步和全局自定义的吐司工具 Tooltip
  6. 开发调试窗口工具 DebugKit
  7. 一行代码创建透明状态栏 StatusBar

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

implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'

常用关键词

关键字描述
navigation导航或者说回退栈
graph导航图
destination目的地, 即在要跳转的页面
pop出栈, 会一直将destination出栈, 直到到达指定id所在页面, 可以理解为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表示不需要回退栈.

手写创建NavHostFragment时并不会自动代码补全, 可以使用Editor Designer创建.

创建资源文件

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可以在编译器校验参数类型安全问题.

image-20190925022025512

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只能使用当前页面在XMl中的节点的子节点action. 不能使用其他的Fragment下的动作.

但是可以通过直接给navigation标签创建子标签action, 这属于全局动作, 即每个Fragment都能使用的动作.

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

    <action
        android:id="@+id/action_categoryFragment_to_main2Activity"
        app:destination="@id/main2Activity" />

    <fragment
        android:id="@+id/categoryFragment"
        android:name="com.example.frame.fragment.CategoryFragment"
        android:label="fragment_category"
        tools:layout="@layout/fragment_category"/>
  
</navigation>

占位页

image-20191001121115585

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

总结

一般情况下我们其实只会在XML中定义Fragment节点.

这些action|argument节点我认为主要是给SafeArgsGradle插件使用. 用于给插件扫描自动生成类比较方便, 如果不使用插件我不建议使用者两个节点, 在文章末尾会详细提及如果使用该插件. 该插件主要是自动处理参数的传递和获取以及目的地跳转.

类关系

NavController 控制导航的跳转和弹出栈

NavOptions 控制跳转过程中的配置选项, 例如动画和singleTop模式

Navigation 工具类 创建点击事件或者获取控制器 (鸡肋), 自己实现扩展函数可能更加方便.

NavHostFragment 导航的容器, 可以设置和获取导航图(NavGraph)

NavGraph 用于描述导航中页面关系的对象 可以增删改查页面,设置起始页等

NavigationUI 用于将导航和一系列菜单控件自动绑定的工具类

Navigator 页面的根接口, 如果想创建一个新的类型页面就要自定义他

NavigatorProvider 一个内部保存着[名称:Navigator]键值对的HashMap. 一般情况不需要使用.

NavDeepLinkBuilder 构建一个能打开导航页面的Intent

NavInflater 用于将XML创建成NavGraph对象, NavController可以通过更方便的函数创建不需要直接使用这个类.

void NavInflater(@NonNull Context context, 
                 @NonNull NavigatorProvider navigatorProvider) // 要求当前NavHostFragment的内部变量
// 构造函数, 但一般情况使用NavController.getNavInflater获取对象

void NavGraph inflate(@NavigationRes int graphResId)

NavBackStackEntry 表示一个目的地所对应

NavController

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

通过三个扩展函数可以得到实例

1. Fragment.findNavController()
2. View.findNavController()
3. Activity.findNavController(viewId: Int) // NavHostFragment的Id

navigate

跳转目的

public void navigate(@IdRes int resId, 
                     @Nullable Bundle args, 
                     @Nullable NavOptions navOptions,
                     @Nullable Navigator.Extras navigatorExtras) // 可设置共享元素动画参数

public void	navigate(Uri deepLink, 
                     NavOptions navOptions, 
                     Navigator.Extras navigatorExtras)
  • resId都是可选参数

通过实现抽象类NavDirections创建自定义的对象来描述跳转目标(action)和传参(bundle)

public void navigate (NavDirections directions, 
                      NavOptions navOptions,
                      Navigator.Extras navigatorExtras)
  • directions都是可选参数

resId可以是XML中的action或者destination节点id, 如果是action则会附带action的配置, 如果是destination则不会附带destination节点下的子节点action(写了白写).

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

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

返回上级

public boolean navigateUp ()
public boolean navigateUp (DrawerLayout drawerLayout)
public boolean navigateUp (AppConfiguration appConfiguration)

出栈

弹出栈, 即从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);
}

DeepLink

NavDeepLinkBuilder createDeepLink ()
// 内部就是调用的DeepLinkBuilder的构造函数

boolean	handleDeepLink(Intent intent)
NavDestination getCurrentDestination ()

NavigatorProvider getNavigatorProvider ()

NavInflater	getNavInflater()

Bundle saveState ()
void restoreState (Bundle navState)
// 用于处理NavController的状态获取和恢复

关于NavGraph函数

void setGraph(NavGraph graph, 
              Bundle startDestinationArgs);

void setGraph(int graphResId, 
              Bundle startDestinationArgs);

NavGraph getGraph ()
  • startDestinationArgs可选参数, 用于传递给起始目的地的参数对象

NavDirections

一个抽象类用于自定义动作和参数, 如果你想复用某个跳转逻辑就可以自定义继承该类. 一般情况下我们并不会手动去继承该类因为过于麻烦, 这个类主要是用于SafeArgs插件自动生成的派生类.

Navigator.Extras

前面提到参数 navigatorExtras 目前是用于支持转场动画的共享元素.

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

fun FragmentNavigatorExtras(vararg sharedElements: Pair<View, String>)

fun ActivityNavigatorExtras(activityOptions: ActivityOptionsCompat? = null, flags: Int = 0)

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

NavOptions

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

XML示例

    <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>

创建NavOptions对象相当于代码动态实现了XML中的<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)

弹出栈

public NavOptions.Builder setPopUpTo (int destinationId, 
                                      boolean inclusive)

函数并不会决定目的地, 只是在导航到目的地之前先执行一个弹出栈指令.

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

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

NavDestination

表示当前目标对象

NavController可以获取NavDestination

findNavController().currentDestination

函数

void	addDeepLink(String uriPattern)
boolean	hasDeepLink(Uri deepLink)

Navigator	getNavigator()
NavGraph	getParent()

NavAction	getAction(int id)
void	putAction(int actionId, NavAction action)
void	putAction(int actionId, int destId)
void	removeAction(int actionId)

void	setDefaultArguments(Bundle args)
void	addDefaultArguments(Bundle args)
Bundle	getDefaultArguments()
  
void	setId(int id)
int	getId()

void	setLabel(CharSequence label)
CharSequence	getLabel()

NavHostFragment

该对象为Navigation提供一个容器

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

public static NavHostFragment create (int graphResId)

UI绑定

Navigation可以与一系列视图组件进行联动

BottomNavigationView

通过扩展函数可以绑定多个导航图, 且关联BNV.

fun BottomNavigationView.setupWithNavController(
    navGraphIds: List<Int>,
    fragmentManager: FragmentManager,
    containerId: Int,
    intent: Intent
): LiveData<NavController>
  • navGraphIds 集合中包含navGrap的Id. 每个BNV的按钮对应一个navGrap.

ActionBar

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

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

fun AppCompatActivity.setupActionBarWithNavController(
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)

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

AppBarConfiguration

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

AppBarConfiguration.Builder(NavGraph navGraph)
AppBarConfiguration.Builder(Menu topLevelMenu)
AppBarConfiguration.Builder(int... topLevelDestinationIds)
AppBarConfiguration.Builder(Set<Integer> topLevelDestinationIds)
// 构造函数效果等同

函数

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

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

源码:

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?
)

Menu

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

fun MenuItem.onNavDestinationSelected(navController: NavController): Boolean =
        NavigationUI.onNavDestinationSelected(this, navController)

使用方式如下:

例如在onOptionsItemSelected函数中设置

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

NavigationView

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

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

查看源码可以知道这些扩展函数本质上只是封装使用NavigationUI的静态函数.

DeepLink

深层链接属于新建回退栈的开启一个页面. Navigation支持两种方式开启DeepLink.

ID开启

很简单

findNavController().createDeepLink()
									.setDestination(R.id.dynamicFragment)
									.createPendingIntent()
									.send()

链接开启

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

AndroidManifest中的对应页面的Activity需要添加以下两个子节点

<activity>
	<action android:name="android.intent.action.VIEW" />
	<nav-graph android:value="@navigation/mobile_navigation" />
</activity>

深层链接

<deepLink app:uri="www.example.com/{myarg}" />
  • http://可以省略

通过ADB测试

adb shell am start -a android.intent.action.VIEW -d "http://www.YourWebsite.com/fromWeb"

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'
    }
}

应用插件

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类.

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

跳转目的地

findNavController().navigate(CategoryFragmentDirections.actionCategoryFragmentToDynamicFragment())

如果你跳转的目的地要求传递参数flag

findNavController().navigate(CategoryFragmentDirections.actionCategoryFragmentToDynamicFragment().setFlag(2))

在Fragment中得到参数

tv.text = PersonInfoFragmentArgs.fromBundle(arguments!!).name

本质上插件就是限制开发者跳转目的地时传递参数时区分可选和必选的传递参数

总结

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

列举下所谓麻烦

  1. Fragment无法设置默认动画, 每个页面都需要单独设置动画.
  2. 所谓Fragment减少内存开销甚至用户都无法感知, 完全没有必要.
  3. 很多框架|功能还是基于Activity实现的(例如路由,状态栏), 可能某些项目架构会受到局限

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