在JetPack中有一个组件是Navigation,顾名思义它是一个页面导航组件,相对于其余的第三方导航,不一样的是它是专门为Fragment的页面管理所设计的。它对于单个Activity的App来讲很是有用,由于以一个Activity为架构的App页面的呈现都是经过不一样的Fragment来展现的。因此对于Fragment的管理相当重要。一般的实现都要本身维护Fragment之间的栈关系,同时要对Fragment的Transaction操做很是熟悉。为了下降使用与维护成本,因此就有了今天的主角Navigation。java
若是你对JetPack的其它组件感兴趣,推荐你阅读我以前的系列文章,本篇文章目前为JetPack系列的最后一篇。node
Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
Paging在RecyclerView中的应用,有这一篇就够了
WorkManager从入门到实践,有这一篇就够了android
对于Navigation的使用,我将其概括于如下四点:git
在使用以前须要引入Navigation的依赖,而后咱们须要为Navigation建立一个配置文件,它将位于res/navigation/nav_graph.xml。为了方便理解文章中的代码,我写了一个Demo,你们能够经过Android精华录查看。github
在个人Demo中打开nav_graph.xml你将清晰的看到它们页面间的关系纽带api
一共有6个页面,最左边的为程序入口页面,它们间的线条指向为它们间可跳转的方向。安全
咱们再来看它们的xm配置👇架构
<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"> <fragment android:id="@+id/welcome_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment" android:label="welcome_fragment" tools:layout="@layout/fragment_welcome"> <action android:id="@+id/action_go_to_register_page" app:destination="@id/register_fragment" /> <action android:id="@+id/action_go_to_order_list_page" app:destination="@id/order_list_fragment"/> </fragment> <fragment android:id="@+id/register_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment" android:label="register_fragment" tools:layout="@layout/fragment_register"> <action android:id="@+id/action_go_to_shop_list_page" app:destination="@id/shop_list_fragment" /> </fragment> ... </navigation>
页面标签主要包含navigation、fragment与actionapp
以上是nav_graph.xml的基本配置。ide
在配置完以后,咱们还须要将其关联到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"> <fragment android: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>
除此以外,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👇
@Override public 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.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中进行配置。👇
<fragment android:id="@+id/shop_list_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopListFragment" android:label="shop_list_fragment" tools:layout="@layout/fragment_shop_list"> <action android:id="@+id/action_go_to_shop_detail_page" app:destination="@id/shop_detail_fragment"> <argument android:name="title" app:argType="string" /> </action> </fragment> <fragment android:id="@+id/shop_detail_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopDetailFragment" android:label="shop_detail_fragment" tools:layout="@layout/fragment_shop_detail"> <action android:id="@+id/action_go_to_cart_page" app:destination="@id/cart_fragment" app:popUpTo="@id/cart_fragment" app:popUpToInclusive="true" /> <argument android: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.title add_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"> <fragment android:id="@+id/welcome_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment" android:label="welcome_fragment" tools:layout="@layout/fragment_welcome"> <action android:id="@+id/action_go_to_register_page" app:destination="@id/register_fragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_in_left" app:popEnterAnim="@anim/slide_out_left" app:popExitAnim="@anim/slide_out_right" /> <action android:id="@+id/action_go_to_order_list_page" app:destination="@id/order_list_fragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_in_left" app:popEnterAnim="@anim/slide_out_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> ... </navigation>
对应四个动画配置参数
经过上面的配置你能够看到以下效果👇
咱们回想一下对于多个Activity我须要实现deepLink效果,应该都是在AndroidManifest.xml中进行配置scheme、host等。而对于单个Activity也须要实现相似的效果,Navigation也提供了对应的实现,并且操做更简单。
Navigation提供的是deepLink标签,能够直接在nav_graph.xml进行配置,例如👇
<fragment android:id="@+id/register_fragment" android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment" android:label="register_fragment" tools:layout="@layout/fragment_register"> <action android:id="@+id/action_go_to_shop_list_page" app:destination="@id/shop_list_fragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_in_left" app:popEnterAnim="@anim/slide_out_left" app:popExitAnim="@anim/slide_out_right" /> <deepLink app:uri="api://register/" /> </fragment>
上面经过deepLink我配置了一个跳转到注册页RegisterFragment,写法很是简单,直接配置uri便可;同时还能够经过占位符配置传递参数,例如👇
<deepLink app:uri="api://register/{id}" />
这时咱们就能够在注册页面经过argument获取key为id的数据。
固然要实现上面的效果,咱们还须要一个前提,须要在AndroidManifest.xml中将咱们的deepLink进行配置,在Activity中使用nav-graph标签👇
<application ... android:theme="@style/AppTheme"> <activity android:name=".navigation.NavigationMainActivity" > <intent-filter> <action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <nav-graph android:value="@navigation/nav_graph"/> </activity> ... </application>
如今只需将文章中的demo安装到手机上,再点击下面的link
以后就会启动App,并定位到注册界面。是否是很是简单呢?
最后咱们再来看下效果👇
有关Navigation暂时就到这里,经过这篇文章,但愿你可以熟悉运用Navigation,而且发现单Activity的魅力。
若是这篇文章对你有所帮助,你能够顺手点赞、关注一波,这是对我最大的鼓励!
该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者可以更快的掌握与理解所阐述的要点