Android Navigation的四大要点你都知道吗?
1
在JetPack中有一个组件是Navigation,顾名思义它是一个页面导航组件,相对于其他的第三方导航,不同的是它是专门为Fragment的页面管理所设计的。它对于单个Activity的App来说非常有用,因为以一个Activity为架构的App页面的呈现都是通过不同的Fragment来展示的。所以对于Fragment的管理至关重要。通常的实现都要自己维护Fragment之间的栈关系,同时要对Fragment的Transaction操作非常熟悉。为了降低使用与维护成本,所以就有了今天的主角Navigation。
如果你对JetPack的其它组件感兴趣,推荐你阅读我之前的系列文章,本篇文章目前为JetPack系列的最后一篇。
Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
Paging在RecyclerView中的应用,有这一篇就够了
对于Navigation的使用,我将其归纳于以下四点:
- Navigation的基本配置
- Navigation的跳转与数据传递
- Navigation的页面动画
- Navigation的deepLink
2
配置
在使用之前需要引入Navigation的依赖,然后我们需要为Navigation创建一个配置文件,它将位于res/navigation/nav_graph.xml。为了方便理解文章中的代码,我写了一个Demo,大家可以通过Android精华录查看。
在我的Demo中打开nav_graph.xml你将清晰的看到它们页面间的关系纽带
一共有6个页面,最左边的为程序入口页面,它们间的线条指向为它们间可跳转的方向。
我们再来看它们的xm配置
1<navigation xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:app="http://schemas.android.com/apk/res-auto"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:id="@+id/nav_graph"
5 app:startDestination="@id/welcome_fragment">
7 <fragment
8 android:id="@+id/welcome_fragment"
9 android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
10 android:label="welcome_fragment"
11 tools:layout="@layout/fragment_welcome">
13 <action
14 android:id="@+id/action_go_to_register_page"
15 app:destination="@id/register_fragment" />
17 <action
18 android:id="@+id/action_go_to_order_list_page"
19 app:destination="@id/order_list_fragment"/>
21 </fragment>
23 <fragment
24 android:id="@+id/register_fragment"
25 android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
26 android:label="register_fragment"
27 tools:layout="@layout/fragment_register">
29 <action
30 android:id="@+id/action_go_to_shop_list_page"
31 app:destination="@id/shop_list_fragment" />
33 </fragment>
35 ...
36</navigation>
页面标签主要包含navigation、fragment与action
- navigation: 定义导航栈,可以进行嵌套定义,各个navigation相互独立。它有一个属性startDestination用来定义导航栈的根入口fragment
- fragment: 顾名思义fragment页面。通过name属性来定义关联的fragment
- action: 意图,可以理解为Intent,即跳转的行为。通过destination来关联将要跳转的目标fragment。
以上是nav_graph.xml的基本配置。
在配置完之后,我们还需要将其关联到Activity中。因为所有的Fragment都离不开Activity。
Navigation为我们提供了两个配置参数: defaultNavHost与navGraph,所以在Activity的xml中需要如下配置
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="@android:color/background_light"
8 android:orientation="vertical"
9 tools:context=".navigation.NavigationMainActivity">
11 <fragment
12 android:id="@+id/nav_host_fragment"
13 android:name="androidx.navigation.fragment.NavHostFragment"
14 android:layout_width="match_parent"
15 android:layout_height="match_parent"
16 app:defaultNavHost="true"
17 app:navGraph="@navigation/nav_graph" />
19</LinearLayout>
- defaultNavHost: 将设备的回退操作进行拦截,并将其交给Navigation进行管理。
- navGraph: Navigation的配置文件,即上面我们配置的nav_graph.xml文件
除此之外,fragment的name属性必须为NavHostFragment,因为它会作为我们配置的所有fragment的管理者。具体通过内部的NavController中的NavigationProvider来获取Navigator抽象实例,具体实现类是FragmentNavigator,所以最终通过它的navigate方法进行创建我们配置的Fragment,并且添加到NavHostFragment的FrameLayout根布局中。
此时如果我们直接运行程序后发现已经可以看到入口页面WelcomeFragment
但点击register等操作你会发现点击跳转无效,所以接下来我们需要为其添加跳转
3
跳转
由于我们之前已经在nav_graph.xml中定义了action,所以跳转的接入非常方便,每一个action的关联跳转只需一行代码
1class WelcomeFragment : Fragment() {
3 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
4 return inflater.inflate(R.layout.fragment_welcome, container, false).apply {
5 register_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_register_page))
6 stroll_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_order_list_page))
7 }
8 }
9}
代码中的id就是配置的action的id,内部原理是先获取到对应的NavController,通过点击的view来遍历找到最外层的parent view,因为最外层的parent view会在配置文件导入时,即NavHostFragment中的onViewCreated方法中进行关联对应的NavController
1 @Override
2 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
3 super.onViewCreated(view, savedInstanceState);
4 if (!(view instanceof ViewGroup)) {
5 throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
6 }
7 Navigation.setViewNavController(view, mNavController);
8 // When added programmatically, we need to set the NavController on the parent - i.e.,
9 // the View that has the ID matching this NavHostFragment.
10 if (view.getParent() != null) {
11 View rootView = (View) view.getParent();
12 if (rootView.getId() == getId()) {
13 Navigation.setViewNavController(rootView, mNavController);
14 }
15 }
16 }
然后再调用navigate进行页面跳转处理,最终通过FragmentTransaction的replace进行Fragment替换
1 -------------- NavController ------------------
3 private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
4 @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
5 boolean popped = false;
6 if (navOptions != null) {
7 if (navOptions.getPopUpTo() != -1) {
8 popped = popBackStackInternal(navOptions.getPopUpTo(),
9 navOptions.isPopUpToInclusive());
10 }
11 }
12 Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
13 node.getNavigatorName());
14 Bundle finalArgs = node.addInDefaultArgs(args);
15 # ---- 关键代码 -------
16 NavDestination newDest = navigator.navigate(node, finalArgs,
17 navOptions, navigatorExtras);
18 ....
19 }
21 -------------- FragmentNavigator ------------------
23 public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
24 @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
25 if (mFragmentManager.isStateSaved()) {
26 Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
27 + " saved its state");
28 return null;
29 }
30 String className = destination.getClassName();
31 if (className.charAt(0) == '.') {
32 className = mContext.getPackageName() + className;
33 }
34 final Fragment frag = instantiateFragment(mContext, mFragmentManager,
35 className, args);
36 frag.setArguments(args);
37 final FragmentTransaction ft = mFragmentManager.beginTransaction();
39 int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
40 int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
41 int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
42 int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
43 if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
44 enterAnim = enterAnim != -1 ? enterAnim : 0;
45 exitAnim = exitAnim != -1 ? exitAnim : 0;
46 popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
47 popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
48 ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
49 }
51 # ------ 关键代码 ------
52 ft.replace(mContainerId, frag);
53 ft.setPrimaryNavigationFragment(frag);
54 ...
55 }
源码就分析到这里了,如果需要深入了解,建议阅读NavHostFragment、NavController、NavigatorProvider与FragmentNavigator
4
传参
以上是页面的无参跳转,那么对于有参跳转又该如何呢?
大家想到的应该都是bundle,将传递的数据填入到bundle中。没错Navigator提供的navigate方法可以进行传递bundle数据?
1findNavController().navigate(R.id.action_go_to_shop_detail_page, bundleOf("title" to "I am title"))
这种传统的方法在传递数据类型上并不能保证其一致性,为了减少人为精力上的错误,Navigation提供了一个Gradle插件,专门用来保证数据的类型安全。
使用它的话需要引入该插件,方式如下
1buildscript {
2 repositories {
3 google()
4 }
5 dependencies {
6 def nav_version = "2.1.0"
7 classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
8 }
9}
最后再到app下的build.gradle中引入该插件
1apply plugin: "androidx.navigation.safeargs.kotlin"
而它的使用方式也很简单,首先参数需要在nav_graph.xml中进行配置。
1 <fragment
2 android:id="@+id/shop_list_fragment"
3 android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopListFragment"
4 android:label="shop_list_fragment"
5 tools:layout="@layout/fragment_shop_list">
7 <action
8 android:id="@+id/action_go_to_shop_detail_page"
9 app:destination="@id/shop_detail_fragment">
11 <argument
12 android:name="title"
13 app:argType="string" />
15 </action>
17 </fragment>
19 <fragment
20 android:id="@+id/shop_detail_fragment"
21 android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopDetailFragment"
22 android:label="shop_detail_fragment"
23 tools:layout="@layout/fragment_shop_detail">
25 <action
26 android:id="@+id/action_go_to_cart_page"
27 app:destination="@id/cart_fragment"
28 app:popUpTo="@id/cart_fragment"
29 app:popUpToInclusive="true" />
31 <argument
32 android:name="title"
33 app:argType="string" />
35 </fragment>
现在我们从ShopListFragment跳转到ShopDetailFragment,需要在ShopListFragment的对应action中添加argument,声明对应的参数类型与参数名,也可以通过defaultValue定义参数的默认值与nullable标明是否可空。对应的ShopDetailFragment接收参数也是一样。
另外popUpTo与popUpToInclusive属性是为了实现跳转到CartFragment时达到SingleTop效果。
下面我们直接看在代码中如何使用这些配置的参数,首先是在ShopListFragment中
1holder.item.setOnClickListener(Navigation.createNavigateOnClickListener(ShopListFragmentDirections.actionGoToShopDetailPage(shopList[position])))
还是创建一个createNavigateOnClickListener,只不过现在传递的不再是跳转的action id,而是通过插件自动生成的ShopListFragmentDirections.actionGoToShopDetailPage方法。一旦我们如上配置了argument,插件就会自动生成一个以[类名]+Directions的类,而自动生成的类本质是做了跳转与参数的封装,源码如下
1class ShopListFragmentDirections private constructor() {
2 private data class ActionGoToShopDetailPage(val title: String) : NavDirections {
3 override fun getActionId(): Int = R.id.action_go_to_shop_detail_page
5 override fun getArguments(): Bundle {
6 val result = Bundle()
7 result.putString("title", this.title)
8 return result
9 }
10 }
12 companion object {
13 fun actionGoToShopDetailPage(title: String): NavDirections = ActionGoToShopDetailPage(title)
14 }
15}
本质是将action id与argument封装成一个NavDirections,内部通过解析它来获取action id与argument,从而执行跳转。
而对于接受方ShopDetailFragment,插件页面自动帮我们生成一个ShopDetailFragmentArgs,以[类名]+Args的类。所以我们需要做的也非常简单
1class ShopDetailFragment : Fragment() {
3 private val args by navArgs<ShopDetailFragmentArgs>()
5 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
6 return inflater.inflate(R.layout.fragment_shop_detail, container, false).apply {
7 title.text = args.title
8 add_cart.setOnClickListener(Navigation.createNavigateOnClickListener(ShopDetailFragmentDirections.actionGoToCartPage()))
9 }
10 }
12}
通过navArgs来获取ShopDetailFragmentArgs对象,它其中包含了传递过来的页面数据。
5
动画
在action中不仅可以配置跳转的destination,还可以定义对应页面的转场动画,使用非
1<?xml version="1.0" encoding="utf-8"?>
2<navigation xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/nav_graph"
6 app:startDestination="@id/welcome_fragment">
8 <fragment
9 android:id="@+id/welcome_fragment"
10 android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
11 android:label="welcome_fragment"
12 tools:layout="@layout/fragment_welcome">
14 <action
15 android:id="@+id/action_go_to_register_page"
16 app:destination="@id/register_fragment"
17 app:enterAnim="@anim/slide_in_right"
18 app:exitAnim="@anim/slide_in_left"
19 app:popEnterAnim="@anim/slide_out_left"
20 app:popExitAnim="@anim/slide_out_right" />
22 <action
23 android:id="@+id/action_go_to_order_list_page"
24 app:destination="@id/order_list_fragment"
25 app:enterAnim="@anim/slide_in_right"
26 app:exitAnim="@anim/slide_in_left"
27 app:popEnterAnim="@anim/slide_out_left"
28 app:popExitAnim="@anim/slide_out_right" />
30 </fragment>
31 ...
32</navigation>
对应四个动画配置参数
- enterAnim: 配置进场时目标页面动画
- exitAnim: 配置进场时原页面动画
- popEnterAnim: 配置回退pop时目标页面动画
- popExitAnim: 配置回退pop时原页面动画
通过上面的配置你可以看到如下效果
6
deepLink
我们回想一下对于多个Activity我需要实现deepLink效果,应该都是在AndroidManifest.xml中进行配置scheme、host等。而对于单个Activity也需要实现类似的效果,Navigation也提供了对应的实现,而且操作更简单。
Navigation提供的是deepLink标签,可以直接在nav_graph.xml进行配置,例如?
1 <fragment
2 android:id="@+id/register_fragment"
3 android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
4 android:label="register_fragment"
5 tools:layout="@layout/fragment_register">
7 <action
8 android:id="@+id/action_go_to_shop_list_page"
9 app:destination="@id/shop_list_fragment"
10 app:enterAnim="@anim/slide_in_right"
11 app:exitAnim="@anim/slide_in_left"
12 app:popEnterAnim="@anim/slide_out_left"
13 app:popExitAnim="@anim/slide_out_right" />
15 <deepLink app:uri="api://register/" />
17 </fragment>
上面通过deepLink我配置了一个跳转到注册页RegisterFragment,写法非常简单,直接配置uri即可;同时还可以通过占位符配置传递参数,例如?
1<deepLink app:uri="api://register/{id}" />
这时我们就可以在注册页面通过argument获取key为id的数据。
当然要实现上面的效果,我们还需要一个前提,需要在AndroidManifest.xml中将我们的deepLink进行配置,在Activity中使用nav-graph标签
1 <application
2 ...
3 android:theme="@style/AppTheme">
4 <activity android:name=".navigation.NavigationMainActivity" >
5 <intent-filter>
6 <action android:name="android.intent.action.VIEW"/>
7 <action android:name="android.intent.action.MAIN" />