共计 12140 个字符,预计需要花费 31 分钟才能阅读完成。
在 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 中的应用,有这一篇就够了
WorkManager 从入门到实践,有这一篇就够了
对于 Navigation 的使用,我将其归纳于以下四点:
- Navigation 的基本配置
- Navigation 的跳转与数据传递
- Navigation 的页面动画
- Navigation 的 deepLink
配置
在使用之前需要引入 Navigation 的依赖,然后我们需要为 Navigation 创建一个配置文件,它将位于 res/navigation/nav_graph.xml。为了方便理解文章中的代码,我写了一个 Demo,大家可以通过 Android 精华录查看。
在我的 Demo 中打开 nav_graph.xml 你将清晰的看到它们页面间的关系纽带
一共有 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 与 action
- 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">
<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>
- 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????
@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>
对应四个动画配置参数
- enterAnim: 配置进场时目标页面动画
- exitAnim: 配置进场时原页面动画
- popEnterAnim: 配置回退 pop 时目标页面动画
- popExitAnim: 配置回退 pop 时原页面动画
通过上面的配置你可以看到如下效果????
deepLink
我们回想一下对于多个 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
jump to register api
之后就会启动 App,并定位到注册界面。是不是非常简单呢?
最后我们再来看下效果????
有关 Navigation 暂时就到这里,通过这篇文章,希望你能够熟悉运用 Navigation,并且发现单 Activity 的魅力。
如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!
项目地址
Android 精华录
该库的目的是结合详细的 Demo 来全面解析 Android 相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点
Android 精华录