全新Fragment- JetPack Navigation 全面介绍

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

若是是使用Java语言我不推荐使用任何新框架了, 就本身玩本身的吧.android

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

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便可查看布局编辑器, 布局编辑器分为三栏.安全

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

navigation这是个嵌套的图表, 能够点击打开新的图表页面.架构

Activity布局中app

<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仍是驾轻就熟的, 而且导航图看起来也颇有逻辑感.

相关文章
相关标签/搜索