写点什么

Android-Navigation 的四大要点你都知道吗?,深入理解 JVM

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日
  • navigation: 定义导航栈,可以进行嵌套定义,各个 navigation 相互独立。它有一个属性 startDestination 用来定义导航栈的根入口 fragment

  • fragment: 顾名思义 fragment 页面。通过 name 属性来定义关联的 fragment

  • action: 意图,可以理解为 Intent,即跳转的行为。通过 destination 来关联将要跳转的目标 fragment。


以上是 nav_graph.xml 的基本配置。


在配置完之后,我们还需要将其关联到 Activity 中。因为所有的 Fragment 都离不开 Activity。


Navigation 为我们提供了两个配置参数: defaultNavHost 与 navGraph,所以在 Activity 的 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"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@android:color/background_light"android:orientation="vertical"tools:context=".navigation.NavigationMainActivity">


<fragmentandroid:id="@+id/nav_host_fragment"android:name="androidx.navigation.fragment.NavHostFragment"android:layout_width="match_parent"android:layout_height="match_parent"app:defaultNavHost="true"app:navGraph="@navigation/nav_graph" />


</LinearLayout>


  • defaultNavHost: 将设备的回退操作进行拦截,并将其交给 Navigation 进行管理。

  • navGraph: Navigation 的配置文件,即上面我们配置的 nav_graph.xml 文件


除此之外,fragment 的 name 属性必须为 NavHostFragment,因为它会作为我们配置的所有 fragment 的管理者。具体通过内部的 NavController 中的 NavigationProvider 来获取 Navigator 抽象实例,具体实现类是 FragmentNavigator,所以最终通过它的 navigate 方法进行创建我们配置的 Fragment,并且添加到 NavHostFragment 的 FrameLayout 根布局中。


此时如果我们直接运行程序后发现已经可以看到入口页面 WelcomeFragment



但点击 register 等操作你会发现点击跳转无效,所以接下来我们需要为其添加跳转

跳转

由于我们之前已经在 nav_graph.xml 中定义了 action,所以跳转的接入非常方便,每一个 action 的关联跳转只需一行代码??


class WelcomeFragment : Fragment() {


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {return inflater.inflate(R.layout.fragment_welcome, container, false).apply {register_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_register_page))stroll_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_order_list_page))}}}


代码中的 id 就是配置的 action 的 id,内部原理是先获取到对应的 NavController,通过点击的 view 来遍历找到最外层的 parent view,因为最外层的 parent view 会在配置文件导入时,即 NavHostFragment 中的 onViewCreated 方法中进行关联对应的 NavController??


@Overridepublic void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);if (!(view instanceof ViewGroup)) {throw new IllegalStateException("created host view " + view + " is not a ViewGroup");}Navigation.setViewNavController(view, mNavController);// When added programmatically, we need to set the NavController on the parent - i.e.,// the View that has the ID matching this NavHostFragment.if (view.getParent() != null) {View rootView = (View) view.getParent();if (rootView.getId() == getId()) {Navigation.setViewNavController(rootView, mNavController);}}}


然后再调用 navigate 进行页面跳转处理,最终通过 FragmentTransaction 的 replace 进行 Fragment 替换??


-------------- NavController ------------------


private void navigate(@NonNull NavDestination node, @Nullable Bundle args,@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {boolean popped = false;if (navOptions != null) {if (navOptions.getPopUpTo() != -1) {popped = popBackStackInternal(navOptions.getPopUpTo(),navOptions.isPopUpToInclusive());}}Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());Bundle finalArgs = node.addInDefaultArgs(args);

---- 关键代码 -------

NavDestination newDest = navigator.navigate(node, finalArgs,navOptions, navigatorExtras);....}


-------------- FragmentNavigator ------------------


public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,@Nullable NavOptions navOptions, @Nullable Navigator.


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


Extras navigatorExtras) {if (mFragmentManager.isStateSaved()) {Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"


  • " saved its state");return null;}String className = destination.getClassName();if (className.charAt(0) == '.') {className = mContext.getPackageName() + className;}final Fragment frag = instantiateFragment(mContext, mFragmentManager,className, args);frag.setArguments(args);final FragmentTransaction ft = mFragmentManager.beginTransaction();


int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {enterAnim = enterAnim != -1 ? enterAnim : 0;exitAnim = exitAnim != -1 ? exitAnim : 0;popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;popExitAnim = popExitAnim != -1 ? popExitAnim : 0;ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);}

------ 关键代码 ------

ft.replace(mContainerId, frag);ft.setPrimaryNavigationFragment(frag);...}


源码就分析到这里了,如果需要深入了解,建议阅读 NavHostFragment、NavController、NavigatorProvider 与 FragmentNavigator

传参

以上是页面的无参跳转,那么对于有参跳转又该如何呢?


大家想到的应该都是 bundle,将传递的数据填入到 bundle 中。没错 Navigator 提供的 navigate 方法可以进行传递 bundle 数据??


findNavController().navigate(R.id.action_go_to_shop_detail_page, bundleOf("title" to "I am title"))


这种传统的方法在传递数据类型上并不能保证其一致性,为了减少人为精力上的错误,Navigation 提供了一个 Gradle 插件,专门用来保证数据的类型安全。


使用它的话需要引入该插件,方式如下??


buildscript {repositories {google()}dependencies {def nav_version = "2.1.0"classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"}}


最后再到 app 下的 build.gradle 中引入该插件??


apply plugin: "androidx.navigation.safeargs.kotlin"


而它的使用方式也很简单,首先参数需要在 nav_graph.xml 中进行配置。??


<fragmentandroid:id="@+id/shop_list_fragment"android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopListFragment"android:label="shop_list_fragment"tools:layout="@layout/fragment_shop_list">


<actionandroid:id="@+id/action_go_to_shop_detail_page"app:destination="@id/shop_detail_fragment">


<argumentandroid:name="title"app:argType="string" />


</action>


</fragment>


<fragmentandroid:id="@+id/shop_detail_fragment"android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopDetailFragment"android:label="shop_detail_fragment"tools:layout="@layout/fragment_shop_detail">


<actionandroid:id="@+id/action_go_to_cart_page"app:destination="@id/cart_fragment"app:popUpTo="@id/cart_fragment"app:popUpToInclusive="true" />


<argumentandroid:name="title"app:argType="string" />


</fragment>


现在我们从 ShopListFragment 跳转到 ShopDetailFragment,需要在 ShopListFragment 的对应 action 中添加 argument,声明对应的参数类型与参数名,也可以通过 defaultValue 定义参数的默认值与 nullable 标明是否可空。对应的 ShopDetailFragment 接收参数也是一样。


另外 popUpTo 与 popUpToInclusive 属性是为了实现跳转到 CartFragment 时达到 SingleTop 效果。


下面我们直接看在代码中如何使用这些配置的参数,首先是在 ShopListFragment 中??


holder.item.setOnClickListener(Navigation.createNavigateOnClickListener(ShopListFragmentDirections.actionGoToShopDetailPage(shopList[position])))


还是创建一个 createNavigateOnClickListener,只不过现在传递的不再是跳转的 action id,而是通过插件自动生成的 ShopListFragmentDirections.actionGoToShopDetailPage 方法。一旦我们如上配置了 argument,插件就会自动生成一个以[类名]+Directions 的类,而自动生成的类本质是做了跳转与参数的封装,源码如下??


class ShopListFragmentDirections private constructor() {private data class ActionGoToShopDetailPage(val title: String) : NavDirections {override fun getActionId(): Int = R.id.action_go_to_shop_detail_page


override fun getArguments(): Bundle {val result = Bundle()result.putString("title", this.title)return result}}


companion object {fun actionGoToShopDetailPage(title: String): NavDirections = ActionGoToShopDetailPage(title)}}


本质是将 action id 与 argument 封装成一个 NavDirections,内部通过解析它来获取 action id 与 argument,从而执行跳转。


而对于接受方 ShopDetailFragment,插件页面自动帮我们生成一个 ShopDetailFragmentArgs,以[类名]+Args 的类。所以我们需要做的也非常简单??


class ShopDetailFragment : Fragment() {


private val args by navArgs<ShopDetailFragmentArgs>()


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {return inflater.inflate(R.layout.fragment_shop_detail, container, false).apply {title.text = args.titleadd_cart.setOnClickListener(Navigation.createNavigateOnClickListener(ShopDetailFragmentDirections.actionGoToCartPage()))}}


}


通过 navArgs 来获取 ShopDetailFragmentArgs 对象,它其中包含了传递过来的页面数据。

动画

在 action 中不仅可以配置跳转的 destination,还可以定义对应页面的转场动画,使用非常简单??


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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android-Navigation的四大要点你都知道吗?,深入理解JVM