Navigation属于具备比较完善回退栈的Fragment导航器. 并且Navigation可以和BottomNavigationView/NavigationView/Toolbar等结合使用, 内部使用的是Fragment的replace
而非show|hidden
, 所以不存在穿透现象. 生命周期存在detachView, 故视图需要自己复用. 虽然Fragment的使用变得比较清晰和方便了, 但是我依旧不推荐使用Fragment去替代Activity. 那只是徒增麻烦而已.
我平时项目开发必备框架
- Android上最强网络请求 Net
- Android上最强列表(包含StateLayout) BRV
- Android最强缺省页 StateLayout
- JSON和长文本日志打印工具 LogCat
- 支持异步和全局自定义的吐司工具 Tooltip
- 开发调试窗口工具 DebugKit
- 一行代码创建透明状态栏 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
可以在编译器校验参数类型安全问题.
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>
占位页
占位页面如果运行时没有指定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还是比较麻烦的.
列举下所谓麻烦
- Fragment无法设置默认动画, 每个页面都需要单独设置动画.
- 所谓Fragment减少内存开销甚至用户都无法感知, 完全没有必要.
- 很多框架|功能还是基于Activity实现的(例如路由,状态栏), 可能某些项目架构会受到局限
我认为Navigation替代FragmentManger还是得心应手的, 并且导航图看起来也很有逻辑感.