原标题:Android Jetpack之Navigation全面剖析
Navigaion 是Android JetPack框架中的一员,是一套新的Fragment管理框架,可以帮助开发者很好的处理fragment之间的跳转,优雅的支持fragment之间的转场动画,支持通过deeplink直接定位到fragment. 通过第三方的插件支持fragment之间安全的参数传递,可以可视化的编辑各个组件之间的跳转关系。导航组件的推出,使得我们在搭架应用架构的时候,可以考虑一个功能模块就是一个Activity, 模块中每个子页面使用Fragment实现,使用Navigation处理Fragment之间的导航。更有甚者,设计一个单Activity的应用也不是没有可能。最后还要提一点,Navigation不只是能管理Fragment,它还支持Activity,小伙伴们请注意这一点。
下面我们来详细介绍下Navigation的使用,在使用之前我们来先了解3个核心概念:
1、Navigation Graph 这是Navigation的配置文件,位于res/navigation/目录下的xml文件. 这个文件是对导航中各个组件的跳转关系的预览。在design模式下,可以很清晰的看到组件之间关系,如图1所示。
2、NavHost 一个空白的父容器,承担展示目的fragment的作用。源码中父容器的实现是NavHostFragment,在Activity中引入这个fragment才能使用Navigation的能力。
3、NavController 导航组件的跳转控制器,管理导航的对象,控制NavHost中目标页面的展示。
下面我们从一个简单的例子先看下Navigation的基本用法。
一 工程搭建
我们设计一个应用,分别实现首页,详情页,购买页,登录页,注册页。跳转关系如下:首页->详情页->购买页->首页,首页->登录页->注册页->首页。如果使用FragmentManager管理,需要对页面创建,参数传递以及页面回退做许多工作,下面我们看一下Navigation是如何管理这些页面的。首先,创建一个空白的工程.只包含一个activity. 修改工程的build.gradle文件使之包含下面的引用
def nav_version =
"2.3.0"
// Java language implementation
implementation
"androidx.navigation:navigation-fragment:
$nav_version
"
implementation
"androidx.navigation:navigation-ui:
$nav_version
"
// Kotlin
implementation
"androidx.navigation:navigation-fragment-ktx:
$nav_version
"
implementation
"androidx.navigation:navigation-ui-ktx:
$nav_version
"
// Dynamic Feature Module Support
implementation
"androidx.navigation:navigation-dynamic-features-fragment:
$nav_version
"
// Testing Navigation
androidTestImplementation
"androidx.navigation:navigation-testing:
$nav_version
"
在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File,此时系统会显示 New Resource File 对话框。在 File name 字段中输入名称,例如“nav_graph”。从 Resource type 下拉列表中选择 Navigation,然后点击 OK,生成的导航的xml (
图1中1位置
)。
在可视化编辑模式下,点击左上角的 icon(
图1中2位置
)在xml中添加导航页面. 添加完导航页面,选中一个页面,在右侧的属性栏,可以为页面添加跳转action, deeplink和跳转传参。直接把两个页面之间连线,也可以建立跳转的action. 选中一条页面间的连线,可以编辑这个action,为action添加转场动画,出栈属性和传参默认值。右键点击一个页面,在右键菜单中选择edit, 就可以编辑对应fragment的xml文件. 都配置完成后,最终的导航图就如图2所示。建立完导航图,我们还需要设置一个当做首页的Fragment一启动就展示,在要设置的Fragment上点击右键,选择Set Start Destination,将它设置为首页,设置完成后,被选中的Fragment会有一个start标签(
图1中3位置
)当Activity启动的时候,它会做为默认的页面替换布局中的NavHostFragment。
下面是nav_graph.xml配置文件部分内容,xml文件如下
<?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/homeFragment"
>
<fragment
android:id=
"@+id/homeFragment"
android:name=
"com.example.navicasetest.HomeFragment"
android:label=
"fragment_home"
tools:layout=
"@layout/fragment_home"
>
<action
android:id=
"@+id/action_homeFragment_to_detailFragment"
app:destination=
"@id/detailFragment"
app:enterAnim=
"@anim/slide_in_right"
app:exitAnim=
"@anim/slide_out_left"
app:popEnterAnim=
"@anim/slide_in_left"
app:popExitAnim=
"@anim/slide_out_right"
/>
<action
android:id=
"@+id/action_homeFragment_to_loginFragment"
app:destination=
"@id/loginFragment"
/>
</fragment>
<!--这里省略其他的fragment的配置-->
</navigation>
通过上面的配置,我们就完整的创建了一个导航图。如下图所示
下面就需要把导航添加到activity中。在MainActivity的xml中,添加Navigation的容器 NavHostFragment, NavHostFragment是系统类,我们后面分析它内部的实现。xml配置如下
<fragment
android:id=
"@+id/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"
我们发现xml中有2个新的配置项,app:navGraph指定导航配置文件。app:defaultNavHost 置为true,标识是让当前的导航容器NavHostFragment处理系统返回键,在 Navigation 容器中如果有页面的跳转,点击返回按钮会先处理 容器中 Fragment 页面间的返回,处理完容器中的页面,再处理 Activity 页面的返回。如果值为 false 则直接处理 Activity 页面的返回。
二 页面跳转和参数传递
页面间的跳转是通过action来实现,我们在HomeFragment中增加detail button的点击响应,实现从首页到详情页的跳转,代码实现如下。这里用到了NavController,我们后面会详细介绍它,这里先看它的用法。
mBtnGoDetail.setOnClickListener(new View.
OnClickListener
{
@Override
public void onClick(View v) {
NavController contorller = Navigation.findNavController(view);
contorller.navigate(R.id.action_homeFragment_to_detailFragment);
下面介绍如何在导航之间传递参数
1、Bundle方式
第一种方式是通过Bundle的方式。NavController 的navigate方法提供了传入参数是Bundle的方法,下面看一下实例代码。从首页传参到商品详情页,首页传入参数
Bundle bundle = new Bundle;
bundle.putString(
"product_name"
,
"苹果"
);
bundle.putFloat(
"price"
,10.5f);
NavController contorller = Navigation.findNavController(view);
contorller.navigate(R.id.action_homeFragment_to_detailFragment, bundle);
if
(getArguments != null) {
mProductName = getArguments.getString(
"product_name"
);
mPrice = getArguments.getFloat(
"price"
);
如果两个fragment直接传递的参数较多,这种传参方法就显得很不友好,需要定义好多名字,并且不能保证传参的一致性,还容易出错或者自定义一个model,实现序列化方法。这样也是比较繁琐。
Android 系统还提供一种SafeArg的传参方式。比较优雅的处理参数的传递。
2、安全参数(SafeArg)
第一步,在工程的build.gradle中添加下面的引用
classpath
"android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"
在app的build.gradle中增加
apply plugin:
'androidx.navigation.safeargs'
第二步,编辑navigation的xml文件 在本例中是nav_graph.xml. 可以通过可视化编辑,也可以直接编辑xml. 编辑完毕如下图
<fragment
android:id=
"@+id/detailFragment"
android:name=
"com.example.myapplication.DetailFragment"
android:label=
"fragment_detail"
tools:layout=
"@layout/fragment_detail"
>
<action
android:id=
"@+id/action_detailFragment_to_payFragment"
app:destination=
"@id/payFragment"
/>
<argument
android:name=
"productName"
app:argType=
"string"
android:defaultValue=
"unknow"
/>
<argument
android:name=
"price"
app:argType=
"float"
android:defaultValue=
"0"
/>
</fragment>
修改完xml后,编译一下工程,在generate文件夹下会生成几个文件。如下图
在首页的跳转函数中,写下如下代码
mBtnGoDetailBySafe.setOnClickListener(new View.
OnClickListener
{
@Override
public void onClick(View v) {
Bundle bundle = new DetailFragmentArgs.Builder.setProductName(
"苹果"
).setPrice(10.5f).build.toBundle;
NavController contorller = Navigation.findNavController(view);
contorller.navigate(R.id.action_homeFragment_to_detailFragment, bundle);
在详情页接收传参的地方,解析传参的代码
Bundle bundle = getArguments;
if
(bundle != null){
mProductName = DetailFragmentArgs.fromBundle(bundle).getProductName;
mPrice = DetailFragmentArgs.fromBundle(bundle).getPrice;
DetailFragmentArgs内部是使用了builder模式构建传参的bundle. 并且以getter,setter的方式设置属性值,这样开发人员使用起来比较简洁,和使用普通java bean的方式基本一致。细心的同学发现了,上面除了DetailFragmentArgs 还生成了2个direction类,我们以HomeFragmentDirections为例看下用法,HomeFragmentDirections能够直接提供跳转的OnClickListener,
mBtnGoDetailBySafe.setOnClickListener(Navigation.createNavigateOnClickListener(HomeFragmentDirections.
actionHomeFragmentToDetailFragment.setProductName(
"苹果"
).setPrice(10.5f)));
分析HomeFragmentDirections代码不难发现,本质是将action id与argument封装成一个NavDirections,内部通过解析它来获取action id与argument,最终还是会执行NavController的navigation方法执行跳转。下面看一下HomeFragmentDirections的内部实现。
@NonNull
public static ActionHomeFragmentToDetailFragment
actionHomeFragmentToDetailFragment
{
return
new ActionHomeFragmentToDetailFragment;
public static class ActionHomeFragmentToDetailFragment implements NavDirections {
private final HashMap arguments = new HashMap;
private
ActionHomeFragmentToDetailFragment
{
@NonNull
public ActionHomeFragmentToDetailFragment setProductName(@NonNull String productName) {
if
(productName == null) {
throw new IllegalArgumentException(
"Argument "productName" is marked as non-null but was passed a null value."
);
this.arguments.put(
"productName"
, productName);
return
this;
@NonNull
public ActionHomeFragmentToDetailFragment setPrice(
float
price) {
this.arguments.put(
"price"
, price);
return
this;
@Override
public int
getActionId
{
return
R.id.action_homeFragment_to_detailFragment;
@SuppressWarnings(
"unchecked"
)
@NonNull
public String
getProductName
{
return
(String) arguments.get(
"productName"
);
@SuppressWarnings(
"unchecked"
)
public
float
getPrice
{
return
(
float
) arguments.get(
"price"
);
3、ViewModel.
导航架构中,也可以通过ViewModel的方式共享数据,后面我们还会讲到使用ViewMode的必要性。每个Destination共享一份ViewModel,这样有利于及时监听数据变化,同时把数据展示和存储隔离。在上面的例子中,每个页面都需要登录状态,我们把用户登录状态封装成UserViewModel,在需要监听登录数据变化的页面实现如下代码
userViewModel.getUserModel.observe(getViewLifecycleOwner, new Observer<UserModel> {
@Override
public void onChanged(UserModel userModel) {
if
(userModel != null){
//登录成功,展示用户名
mUserName.setText(userModel.getUserName);
}
else
{
mUserName.setText(
"未登录"
);
这样当用户登录后,各个页面都会得到通知,刷新当前的昵称展示。
多数场景下,2个页面之间的切换,我们希望有转场动画,Navigation对动画的支持也很简单。可以在xml中直接配置配置。
<fragment
android:id=
"@+id/homeFragment"
android:name=
"com.example.navicasetest.HomeFragment"
android:label=
"fragment_home"
tools:layout=
"@layout/fragment_home"
>
<action
android:id=
"@+id/action_homeFragment_to_detailFragment"
app:destination=
"@id/detailFragment"
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>
enterAnim: 配置进场时目标页面动画 exitAnim: 配置进场时原页面动画 popEnterAnim: 配置回退时目标页面动画 popExitAnim: 配置回退时原页面动画 配置完后,动画展示如下
四 导航堆栈管理
Navigation 有自己的任务栈,每次调用navigate函数,都是一个入栈操作,出栈操作有以下几种方式,下面详细介绍几种出栈方式和使用场景。
1、系统返回键
首先需要在xml中配置app:defaultNavHost="true",才能让导航容器拦截系统返回键,点击系统返回键,是默认的出栈操作,回退到上一个导航页面。如果当栈中只剩一个页面的时候,系统返回键将由当前Activity处理。
2、自定义返回键
如果页面上有返回按钮,那么我们可以调用popBackStack或者navigateUp返回到上一个页面。我们先看一下navigateUp源码
public boolean
navigateUp
{
if
(getDestinationCountOnBackStack == 1) {
// If there
's only one entry, then we'
ve deep linked into a specific destination
// on another task so we need to find the parent and start our task from there
NavDestination currentDestination = getCurrentDestination;
int destId = currentDestination.getId;
NavGraph parent = currentDestination.getParent;
while
(parent != null) {
if
(parent.getStartDestination != destId) {
//省略部分代码
return
true
;
destId = parent.getId;
parent = parent.getParent;
// We
're already at the startDestination of the graph so there'
s no
'Up'
to go to
return
false
;
}
else
{
return
popBackStack;
从源码可以看出,当栈中任务大于1个的时候,两个函数没什么区别。当栈中只有一个导航首页(start destination)的时候,navigateUp不会弹出导航首页,它什么都不做,直接返回false. popBackStack则会把导航首页也出栈,但是由于没有回退到任何其他页面,此时popBackStack会返回false, 如果此时又继续调用navigate函数,会发生exception。所以google官网说不建议把导航首页也出栈。如果导航首页出栈了,此时需要关闭当前Activity。或者跳转到其他导航页面。示例代码如下。
if
(!navController.popBackStack) {
// Call finish on your Activity
finish;
3、popUpTo 和 popUpToInclusive
还有一种出栈方式,就是通过设置popUpTo和popUpToInclusive在导航过程中弹出页面。popUpTo指出栈直到某目标,字面意思比较难理解,我们看下面这个例子。假设有A,B,C 3个页面,跳转顺序是 A to B,B to C,C to A。依次执行几次跳转后,栈中的顺序是A>B>C>A>B>C>A。此时如果用户按返回键,会发现反复出现重复的页面,此时用户的预期应该是在A页面点击返回,应该退出应用。此时就需要在C到A的action中设置popUpTo="@id/a". 这样在C跳转A的过程中会把B,C出栈。但是还会保留上一个A的实例,加上新创建的这个A的实例,就会出现2个A的实例. 此时就需要设置 popUpToInclusive=true. 这个配置会把上一个页面的实例也弹出栈,只保留新建的实例。下面再分析一下设置成false的场景。还是上面3个页面,跳转顺序A to B,B to C. 此时在B跳C的action中设置 popUpTo=“@id/a”, popUpToInclusive=false. 跳到C后,此时栈中的顺序是AC。B被出栈了。如果设置popUpToInclusive=true. 此时栈中的保留的就是C。AB都被出栈了。在咱们的示例中,在注册界面,用户注册完成后,希望直接返回首页。这样我们就需要在从RegisterFragment到HomeFragment的跳转过程中,弹出之前栈中的首页,登录页和注册页,添加如下配置既可达到我们想要的效果。
<fragment
android:id=
"@+id/registerFragment"
android:name=
"com.example.navicasetest.RegisterFragment"
android:label=
"fragment_register"
tools:layout=
"@layout/fragment_reg"
>
<action
android:id=
"@+id/action_registerFragment_to_homeFragment"
app:destination=
"@id/homeFragment"
app:popUpTo=
"@id/homeFragment"
app:popUpToInclusive=
"true"
/>
</fragment>
五 DeepLink
Navigation组件提供了对深层链接(DeepLink)的支持。通过该特性,我们可以利用PendingIntent或者一个真实的URL链接,直接跳转到应用程序的某个destination 下面我们分别看一下这两种的使用方式。
1、PendingIntent
创建一个通知栏,通过Navigition 创建PendingIntent.
private void
createNotification
{
if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"ChannelName"
, importance);
channel.setDeion(
"deion"
);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(
"促销水果"
)
.setContentText(
"香蕉"
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(getPendingIntent)//设置PendingIntent
.setAutoCancel(
true
);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(100001, builder.build);
private PendingIntent
getPendingIntent
{
Bundle bundle = new Bundle;
bundle.putString(
"productName"
,
"香蕉"
);
bundle.putFloat(
"price"
,6.66f);
return
Navigation
.findNavController(this,R.id.fragment)
.createDeepLink
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.detailFragment)
.setArguments(bundle)
.createPendingIntent;
在DetailFragment, 解析传参即可。参考上面的传参小节。效果如下所示
2、URL连接
URL的使用也比较简单,我们下面给商品详情页(DetailFragment)添加deeplink支持,URL格式如下。www.mywebsite.com/detail?productName={productName}price={price} 首先,需要在导航xml中,添加deeplink支持,添加完成xml如下
<fragment
android:id=
"@+id/detailFragment"
android:name=
"com.example.navicasetest.DetailFragment"
android:label=
"fragment_detail"
tools:layout=
"@layout/fragment_detail"
>
<action
android:id=
"@+id/action_detailFragment_to_payFragment"
app:destination=
"@id/payFragment"
app:enterAnim=
"@anim/slide_in_right"
app:exitAnim=
"@anim/slide_out_left"
app:popEnterAnim=
"@anim/slide_in_left"
app:popExitAnim=
"@anim/slide_out_right"
/>
<argument
android:name=
"productName"
android:defaultValue=
"unknow"
app:argType=
"string"
/>
<argument
android:name=
"price"
android:defaultValue=
"0.0f"
app:argType=
"float"
/>
<deepLink
android:autoVerify=
"true"
app:uri=
"www.mywebsite.com/detail?productName={productName}price={price}"
/>
</fragment>
然后,在Manifest文件中,添加如下配置
<nav-graph android:value=
"@navigation/nav_graph"
/>
我们的DetailFragment中已经做了对参数productName和price的解析。安装app后,使用adb 命令测试deeplink连接
adb shell am start -a android.intent.action.VIEW -d
"http://www.mywebsite.com/detail?productName="
香蕉
"price=10"
执行adb命令后,商品详情页被正常拉起。
六 场景对比
上面介绍了Navigation的基本用法,这一小节我们将构建一个页面,分别看一下使用Navigation和不使用Navigation对页面架构的影响。在我们以往的项目开发过程中, 业务复杂且包含的模块比较多的页面, 我们经常用独立的fragment来承担不同的业务子页面,但是fragment之间的跳转,转场动画,以及回退栈管理,开发者需要自己实现相关逻辑。我们看下面的例子:
实现上面包含3个tab的首页,常规做法是使用BottomNavigationView + fragment来搭架。代码如下, 需要自己管理fragment的创建以及加载。
public class MainActivity2 extends AppCompatActivity {
private int laseSelectPos = 0;
private Fragment[] fragments;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
HomeFragment homeFragment = new HomeFragment;
DashboardFragment dashboardFragment = new DashboardFragment;
NotificationsFragment notificationsFragment = new NotificationsFragment;
fragments = new Fragment[]{homeFragment, dashboardFragment, notificationsFragment};
laseSelectPos = 0;
getSupportFragmentManager
.beginTransaction
.add(R.id.fl_con, homeFragment)
.show(homeFragment)//展示
.commit;
BottomNavigationView navView = findViewById(R.id.nav_vew_2);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.
OnNavigationItemSelectedListener
{
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId){
case
R.id.navigation_home:
if
(0 != laseSelectPos) {
setDefaultFragment(0);
laseSelectPos = 0;
return
true
;
case
R.id.navigation_dashboard:
if
(1 != laseSelectPos) {
setDefaultFragment(1);
laseSelectPos = 1;
return
true
;
case
R.id.navigation_notifications:
if
(2 != laseSelectPos) {
setDefaultFragment(2);
laseSelectPos = 2;
return
true
;
return
false
;
private void setDefaultFragment( int index) {
FragmentTransaction transaction = getSupportFragmentManager.beginTransaction;
transaction.replace(R.id.fl_con, fragments[index]);
transaction.commit;
配置文件如下:
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<androidx.constraintlayout.widget.ConstraintLayout
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:paddingTop=
"?attr/actionBarSize"
>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id=
"@+id/nav_vew_2"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"0dp"
android:layout_marginEnd=
"0dp"
android:background=
"?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toStartOf=
"parent"
app:menu=
"@menu/bottom_nav_menu"
/>
<FrameLayout
android:id=
"@+id/fl_con"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
app:layout_constraintBottom_toTopOf=
"@+id/nav_vew_2"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toStartOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
如果我们使用Navigation + BottomNavigationView来搭建上述要页面 代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
// Passing each menu ID as a
set
of Ids because each
// menu should be considered as top level destinations.
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
.build;
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView, navController);
配置文件如下
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
android:id=
"@+id/container"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:paddingTop=
"?attr/actionBarSize"
>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id=
"@+id/nav_view"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"0dp"
android:layout_marginEnd=
"0dp"
android:background=
"?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintLeft_toLeftOf=
"parent"
app:layout_constraintRight_toRightOf=
"parent"
app:menu=
"@menu/bottom_nav_menu"
/>
<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:layout_constraintBottom_toTopOf=
"@id/nav_view"
app:layout_constraintLeft_toLeftOf=
"parent"
app:layout_constraintRight_toRightOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
app:navGraph=
"@navigation/mobile_navigation"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
比较上面2份代码,明显Navigation的方式实现更简洁,框架帮我们做了好多创建和管理的工作,我们只要专注每个fragment的业务即可。例子中只是单纯的展示fragment, 后面如果要加deeplink跳转,转场动画等需求,就会更加体现navigation优势。
七 源码分析
Navigation暴露给开发者的就是NavHostFragment,NavController以及导航图。导航图又再xml文件中设置给了NavHostFragment。所以我们就主要分析这两个类NavHostFragment和NavController。我们带着下面几个问题来分析下源码:
导航图是如何解析?
页面跳转是如何实现的?
为什么从一个静态方法随便传入一个view,就能拿到NavController实例?
导航框架不仅支持fragment还支持activity, 是如何做到的?
为了避免大量的代码影响阅读体验,后面的源码分析只把关键的代码做了展示,本文中未列出的代码,读者可以自行参考源码。
1、NavHostFragment
要在某个Activity中实现导航,首先就是要在xml中引入NavHostFragment,xml中通过指定app:navGraph="@navigation/nav_graph"来指定导航图, 那么应该是这个Fragment来负责解析并加载导航图。我们就从这个Fragment创建流程入手,来看一下源码。1、onInflate 在这个流程中解析出我们上面提到的在xml配置的两个参数defaultNavHost, 和navGraph,并保存在成员变量中 mGraphId,mDefaultNavHost。
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if
(graphId != 0) {
mGraphId = graphId;
navHost.recycle;
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost,
false
);
if
(defaultHost) {
mDefaultNavHost =
true
;
a.recycle;
2、onCreate, 在OnCreate中,我们发现了NavController是在这里创建的, 这就说明一个导航图对应一个NavController,在OnCreate中还把上面的mGraphId,设置给了NavController.
mNavController = new NavHostController(context);
//省略部分代码
if
(mGraphId != 0) {
// Set from onInflate
mNavController.setGraph(mGraphId);
}
else
{
// See
if
it was
set
by NavHostFragment.create
final Bundle args = getArguments;
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if
(graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
3、onCreateView 在这个函数中,只是创建了一个FragmentContainerView. 这个View是一个FrameLayout, 用于加载导航的Fragment
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext);
// When added via XML, this has no effect (since this FragmentContainerView is given the ID
// automatically), but this ensures that the View exists as part of this Fragment
's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
containerView.setId(getContainerId);
return containerView;
4、onViewCreated 在这个函数中,把NavController设置给了父布局的view的中的ViewTag中。这里的设计比较关键,为什么要放到tag中呢?其实这样的设计是为了让我们外部获取这个实例比较便捷,我们上面的问题3的答案就在这里,我们先看一下查找NavController的函数Navigation.findNavController(View),请注意API的设计,似乎传递任意一个 view的引用都可以获取 NavController,这里就是通过递归遍历view的父布局,查找是否有view含有id为R.id.nav_controller_view_tag的tag, tag有值就找到了NavController。如果tag没有值.说明当前父容器没有NavController.这里我们贴一下保存和查找的代码。
public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
@Nullable
private static NavController findViewNavController(@NonNull View view) {
while
(view != null) {
NavController controller = getViewNavController(view);
if
(controller != null) {
return
controller;
ViewParent parent = view.getParent;
view = parent instanceof View ? (View) parent : null;
return
null;
以上4步,就是NavHostFragment的主要工作,我们通过上面的分析可以看到,这个Fragment没有承担任何Destination的创建和导航工作。也没有看到导航图的解析工作,这个Fragment只是创建了个容器,创建了NavController,然后把只是单纯的把mGraphId设置给了NavController。我们猜测导航的解析和创建工作应该都在NavController中。我们来看一下NavController的源码。
2、NavController
导航的主要工作都在NavController中,涉及xml解析,导航堆栈管理,导航跳转等方面。下面我们带着上面剩余的3个问题,分析下NavController的实现。
上面我们提到NavHostFragment把导航文件的资源id传给了NavController,我们继续分析代码发现,NavController把导航xml文件传递给了NavInflater, NavInflater主要负责解析导航xml文件,解析完毕后,生成NavGraph,NavGraph是个目标管理容器,保存着xml中配置的导航目标NavDestination。
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName);
final NavDestination dest = navigator.createDestination;
dest.onInflate(mContext, attrs);
final int innerDepth = parser.getDepth + 1;
int
type
;
int depth;
while
((
type
= parser.next) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth) >= innerDepth
||
type
!= XmlPullParser.END_TAG)) {
if
(
type
!= XmlPullParser.START_TAG) {
continue
;
if
(depth > innerDepth) {
continue
;
final String name = parser.getName;
if
(TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
}
else
if
(TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
}
else
if
(TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId);
}
else
if
(TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(
attrs, androidx.navigation.R.styleable.NavInclude);
final int id = a.getResourceId(
androidx.navigation.R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle;
}
else
if
(dest instanceof NavGraph) {
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
return
dest;
导航目标解析完毕,具体的页面跳转是如何实现的呢,在使用过程中我们调用的是NavController的navigate函数,抽丝剥茧,发现导航最终调用的是Navigator的navigate函数。
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName);
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
我们看到导航的具体实现是Navigator,我们上面的例子是以Fragment为导航目标,但是Navigation 的目标对象不只是Fragment, 还可以是Activity,后面可能还会扩展其他种类, 这里谷歌把导航抽象成了Navigator,NavController中没有持有具体的导航种类,而是持有的抽象类Navigator, 把所有Navigator的实例保存在了NavigatorProvider中. 这里就运用了设计模式中的依赖倒置原则,要面向接口编程,而不是具体实现。同时也符合了开闭原则,后面在扩展新的导航种类,不会影响到现有的种类。通过以上的分析,问题2和问题4也就得到了解答。我们以FragmentNavigator为例,看一下具体的导航逻辑的实现。只分析部分关键代码片段
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;
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
从以上代码可以看出,Fragment实例是通过instantiateFragment创建的,这个函数中是通过反射的方式创建的Fragment实例,Fragment还是通过FragmentManager进行管理,是用replace方法替换新的Fragment, 这就是说每次导航产生的Fragment都是一个新的实例,不会保存之前Fragment的状态。这样的话,可能会造成数据不同步的现象。所以google建议导航和ViewModel配合使用效果更佳。
综上所述,NavController是导航的核心类,它负责页面加载,页面导航,和堆栈管理。但是这些逻辑没有都耦合在这个类中,而是采用组合的方式,把这些实现都拆分成了单独的模块。NavController需要实现哪些功能,调用相应功能即可。
上面我们列举了导航的基本用法以及源码分析,通过上面的学习,大家也了解到了,导航组件是一个页面的管理框架,创建简洁,使用方便,在构架业务复杂的页面时,架构清晰,功能多样,可以使开发者可以专注于业务逻辑的开发,是一个优秀的框架。我们在学习的过程中,不仅要学会如何使用,还要深入的学习其架构原理,为我们以后的项目架构,提供可借鉴的方案。
参考文献:https://developer.android.google.cn/guide/navigation/navigation-getting-started https://www.jianshu.com/p/ad040aab0e66
返回搜狐,查看更多