引用Android官方文档的一句话:
The Navigation component is designed for apps that have one main activity with multiple fragment destinations.
Navigation组件是为了那些使用"单Activity多Fragment"架构模式的app而设计,在这个系列教程里我们只使用Fragment
作为目的地去管理界面和实现界面跳转
build.gradle
文件buildscript {
repositories {
google()
}
dependencies {
// safe args gradle插件版本号
def nav_version = "2.4.1"
// 依赖safe args gradle插件。用于自动生成目的地之间传递参数需要的class文件
// 通过它自动生成的代码我们可以方便快捷地在导航的时候传递参数
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
build.gradle
文件plugins {
// 应用safe args gradle插件
id 'androidx.navigation.safeargs'
}
dependencies {
// Navigation组件版本号,推荐使用最新的稳定版本号
def nav_version = "2.4.1"
// Java和Kotlin二选一即可
// 依赖Java语言对应的navigation组件
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// 依赖Kotlin语言对应的navigation组件
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
在res
目录下创建一个Navigation
类型的资源文件:
res
目录->New->Android Resource Fileres
目录下创建一个navigation
目录,里面包含你创建的导航图资源文件nav_graph.xml
,它的根元素是
NavHost是一个接口,实现了NavHost接口的类称为导航宿主。
让我们看看Android官方文档是如何解释导航宿主的:
The navigation host is an empty container where destinations are swapped in and out as a user navigates through your app.
直接翻译:导航宿主是一个空的容器,当用户在你的app中导航时,目的地会在其中换入和换出。例如:我们在app里从A目的地导航到B目的地的时候,导航组件会把在导航宿主中的A目的地换出,然后把B目的地换入导航宿主
总结翻译:导航宿主(NavHost)是一个目的地容器
直接使用NavHostFragment
还是Android官方文档的说明:
A navigation host must derive from NavHost. The Navigation component’s default NavHost implementation, NavHostFragment, handles swapping fragment destinations.
意思就是说:导航宿主必须派生自NavHost
接口。在导航组件里有一个默认的
实现了NavHost接口的类叫NavHostFragment
,这个NavHostFragment
作为导航宿主,它既作为Fragment目的地的容器,又处理Fragment目的地之间的交换
总结:当我们只使用Fragment作为目的地时,不需要再写一个类去实现NavHost接口,直接使用NavHostFragment即可满足需求。
添加NavHostFragment到Activity的xml文件中
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
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" />
androidx.constraintlayout.widget.ConstraintLayout>
属性说明:
android:name="androidx.navigation.fragment.NavHostFragment"
表示使用NavHostFragment作为NavHost
app:defaultNavHost="true"
表示拦截系统的返回按钮事件
app:navGraph="@navigation/nav_graph"
表示NavHostFragment关联的导航图为nav_graph.xml。这个导航图指定了在此NavHostFragment里面,我们可以导航到的所有目的地。这里nav_graph.xml就是我们刚刚创建的那个导航图
分别创建AFragment
和BFragment
作为导航的目的地。
public class AFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_a, container, false);
}
}
public class BFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_b, container, false);
}
}
通过
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/aFragment">
<fragment
android:id="@+id/aFragment"
android:name="com.scx.navigation.basicuse.AFragment"
android:label="aFragment" />
<fragment
android:id="@+id/bFragment"
android:name="com.scx.navigation.basicuse.BFragment"
android:label="bFragment" />
navigation>
通过
假设我们的需求是从AFragmet
跳转到BFragment
,那么我们需要在AFragment
目的地里面添加
元素
<fragment
android:id="@+id/aFragment"
android:name="com.scx.navigation.basicuse.AFragment"
android:label="aFragment" >
<action
android:id="@+id/action_aFragment_to_bFragment"
app:destination="@id/bFragment" />
fragment>
此时,safe args gradle插件会生成AFragmentDirections
类,包含actionAFragmentToBFragment()
方法和ActionAFragmentToBFragment
内部类。
AFragmentDirections类:
// 格式为`目的地类名+Directions`。方法名和内部类名都是actionId的驼峰式写法
public class AFragmentDirections {
// 返回值为ActionAFragmentToBFragment(NavDirections)对象
public static ActionAFragmentToBFragment actionAFragmentToBFragment(){}
// NavDirections实现类
public static class ActionAFragmentToBFragment implements NavDirections {}
}
NavDirections接口:
/**
* An interface that describes a navigation operation: action's id and arguments
*/
public interface NavDirections {
/**
* An action id to navigate with.
*/
@get:IdRes
public val actionId: Int
/**
* Arguments to pass to the destination
*/
public val arguments: Bundle
}
NavDirections主要有两个作用:
使用NavController+NavDirections导航到目的地。
通过前面的学习我们知道导航宿主(NavHostFragment
)是一个容纳和交换目的地的容器,其实它本身并不会执行导航的操作,那我们想导航到目的地应该怎么做?
答案是使用NavController
的navigate(navDirections:NavDirections)
方法。
顾名思义NavController是一个导航控制管理器。每个导航宿主都有其对应的NavController。
代码如下:
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 点击屏幕跳转
view.findViewById(R.id.tv_a).setOnClickListener(v -> {
// 获取NavController
NavController navController = NavHostFragment.findNavController(AFragment.this);
/*
* 通过NavController+NavDirections执行导航操作
* 通过AFragmentDirections.actionAFragmentToBFragment()获取NavDirections对象
*/
navController.navigate(AFragmentDirections.actionAFragmentToBFragment());
});
}
好了,这样就实现了最基本的导航功能了,是不是超级简单?但显然这无法满足我们实际开发的需求,起码应该能够像Activity一样在跳转的时候传递参数。
通过
假设BFragment需要知道前一个目的地是谁,那么可以这样写
<fragment
android:id="@+id/bFragment"
android:name="com.scx.navigation.basicuse.BFragment"
android:label="bFragment" >
<argument
android:name="fromWho"
app:argType="string" />
fragment>
只需要在BFragment目的地里面加一个
元素,android:name
指定参数的名称,app:argType
指定参数的类型。通过Android官方文档可以知道safe args gradle插件支持以下参数类型
类型 | app:argType语法 | 是否支持默认值 | 是否支持路由 | 是否可以为空 |
---|---|---|---|---|
Integer | app:argType=“integer” | Yes | Yes | No |
Float | app:argType=“float” | Yes | Yes | No |
Long | app:argType=“long” | Yes - 默认值必须以 ‘L’ 作为后缀(e.g. “123L”). | Yes | No |
Boolean | app:argType=“boolean” | Yes - “true” or “false” | Yes | No |
String | app:argType=“string” | Yes | Yes | Yes |
资源引用 | app:argType=“reference” | Yes - 默认值格式必须为 “@resourceType/resourceName” (e.g. “@style/myCustomStyle”) 或者 “数字” | Yes | No |
自定义Parcelable | app:argType=" |
只支持 "@null"作为默认值 | No | Yes |
自定义Serializable | app:argType=" |
只支持 "@null"作为默认值 | No | Yes |
自定义Enum | app:argType=" |
Yes - 默认值必须为非限定名(e.g. “SUCCESS” to match MyEnum.SUCCESS). | No | No |
数组 | app:argType=" |
Yes-只支持 "@null"作为默认值,但必须添加app:nullable="true"属性 | No | Yes |
注意:参数应尽可能地轻量化。通过参数传递复杂的数据结构被认为是一种反面模式。 每个目的地都应根据最少的必要信息(例如Item ID)加载 UI 数据。 这简化了导航流程重建并避免了潜在的数据不一致。
声明参数之后,safe args gradle插件会立马做两件事件:
(1).在源目的地的NavDirections内添加该参数
此时的AFragmentDirections类:
public class AFragmentDirections {
// 必须传入String类型的fromWho参数。由于fromWho没有默认值,所以必须要传参,详情见参数默认值章节
public static ActionAFragmentToBFragment actionAFragmentToBFragment(String fromWho){}
// NavDirections实现类
public static class ActionAFragmentToBFragment implements NavDirections {
// 设置fromWho参数值(一般不需要手动调用,上面的actionAFragmentToBFragment()方法已经设置了该参数)
public ActionAFragmentToBFragment setFromWho(String fromWho){}
}
}
(2).生成一个Ags类用来接收参数,类名格式为目的地类名+Args
我们这个例子的Ags类就是BFragmentArgs,它实现了NavArgs
接口
BFragmentArgs类:
public class BFragmentArgs implements NavArgs {
// 接收参数
public static BFragmentArgs fromBundle(@NonNull Bundle bundle) {}
// 获取fromWho参数值
public String getFromWho(){}
// 构造参数的Builder,在后面介绍的通过id导航传参会用到,可先跳过
public static final class Builder {
// fromWho没有默认值,必须设置它的值
public Builder(String fromWho){}
public Builder setFromWho(String fromWho){}
}
}
下面分别通过这两个类传递和接收参数
通过NavDirections传递参数。
直接通过AFragmentDirections的actionAFragmentToBFragment()方法传递参数
// 把字符串“A”传递给BFragment
navController.navigate(AFragmentDirections.actionAFragmentToBFragment("A"));
通过NavArgs接收参数
在BFragment里面使用BFragmentArgs接收参数
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 获取接收参数类对象,获取方法是固定的
BFragmentArgs args = BFragmentArgs.fromBundle(requireArguments());
// 获取fromWho参数
String fromWho = args.getFromWho();
// 获取showFromWho参数
boolean showFromWho = args.getShowFromWho();
TextView textView = view.findViewById(R.id.tv_from_who);
// 如果showFromWho参数为true则显示fromWho
if (showFromWho) {
textView.setText("from:" + fromWho);
}
}
等等,这里为什么还有一个showFromWho
参数?请看下文。
通过android:defaultValue属性给参数设置默认值。
我们再给BFragment目的地添加一个参数并设置默认值,这个参数决定是否将fromWho参数展示在屏幕上,默认为false
。显然在导航的时候这个参数可传可不传。
<argument
android:name="showFromWho"
app:argType="boolean"
android:defaultValue="false"/>
同样,safe args gradle会修改AFragmentDirections和BFragmentArgs两个类。
AFragmentDirections类:
public class AFragmentDirections {
// 必须传入String类型的fromWho参数。由于fromWho没有默认值,所以必须要传参,而showFromWho有默认值,这里不传参
public static ActionAFragmentToBFragment actionAFragmentToBFragment(String fromWho){}
// NavDirections实现类
public static class ActionAFragmentToBFragment implements NavDirections {
// 设置fromWho参数值(一般不需要手动调用,上面的actionAFragmentToBFragment()方法已经设置了该参数)
public ActionAFragmentToBFragment setFromWho(String fromWho){}
// 设置showFromWho参数值,由于showFromWho有默认值,所以必须手动调用修改参数值
public ActionAFragmentToBFragment setShowFromWho(boolean showFromWho){}
}
}
BFragmentArgs接收参数的方式不变,所以不再展示。
通过Directions类的NavDirections内部类修改默认值。
相信通过上面的AFragmentDirections代码,修改参数默认值的方法显而易见,它类似于Builder模式,代码如下
navController.navigate(AFragmentDirections.actionAFragmentToBFragment("A").setShowFromWho(true));
当然也可以这样写,这两者是等价的,也许能帮助理解
AFragmentDirections.ActionAFragmentToBFragment action = AFragmentDirections.actionAFragmentToBFragment("A");
action.setShowFromWho(true);
navController.navigate(action);
Bundle bundle = new BFragmentArgs.Builder("A")
.setShowFromWho(true)
.build()
.toBundle();
// 通过目的地id导航,通过bundle传递参数
navController.navigate(R.id.bFragment, bundle);
// 通过actionId导航,通过bundle传递参数
navController.navigate(R.id.action_aFragment_to_bFragment, bundle);
NavigationBasicUse
代码地址
本篇文章介绍的只是最基本的使用方法,但却是极其重要的,它是进一步使用Navigation组件的基石。我会在后面的文章中继续介绍导航组件的进阶使用和它的一些原理,往后还会介绍我在实际项目中遇到的问题和解决方法。
水平有限,如有不对,欢迎指正。有什么问题也欢迎在评论区留言。