前言
导航(Navigation )指的是用户在应用中的前进和后退操作。导航很大程度上和业务逻辑相关。但是,在传统的Android应用开发中,导航需要开发者自己处理,对应的backstack也需要开发者自行维护,与业务相关的导航逻辑也要硬编码在Java代码中。而同时期的iOS应用开发框架早就提出了导航图的概念,并对导航进行了框架级别的抽象和支持。
为了解决类似问题,简化Android应用开发,Google在2018年推出了Jetpack。Jetpack是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。在Jetpack中,Android第一次以Navigation组件的形式对用户的导航行为进行了抽象和封装,不仅简化了开发流程,还通过遵循一套既定原则来确保一致且可预测的用户体验,从而实现了代码可维护性和用户体验一致性的完美结合。
概览
Jetpack导航组件由以下三个关键部分组成:
- 导航图(Navigation graph):在一个XML文件中集中定义所有导航相关信息的,它包括应用内的所有导航目标以及所有可能的导航路径。
- 导航容器(NavHost):显示导航目标的容器。Jetpack提供了一个默认的导航容器实现NavHostFragment。
- 导航控制器(NavController):在导航容器中管理导航对象。当用户在应用中前进或者后退时,导航控制器决定下一个将要显示的内容。
功能
JetpackNavigation组件提供以下几个功能:
- 封装Fragment事务:Navigation组件封装了Fragment事务,从而简化了开发者的工作。
- 为过场动画提供标准化支持:开发者只需要专注于定义过场动画本身。
- 传递参数:通过Safe Args插件,开发者可在导航目标之间传递类型安全的数据。
实战
我们的实战是基于Google的Jetpack Navigation Codelab。它的最终效果是这样:
这是3个简单的Fragment之间跳转的情景,经过过场动画的修饰,它们之前的切换非常流畅自然。接下来,我们就通过代码来详细讲解Fragment导航是如何实现的。
首先,在build.gradle中添加以下依赖:
dependencies {
...
//Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}
新建两个Fragment。其中,第二个Fragment会根据传入的参数决定要加载的UI:
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.home_fragment, container, false)
}
}
class FlowStepFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return when (flowStepNumber) {
2 -> inflater.inflate(R.layout.flow_step_two_fragment, container, false)
else -> inflater.inflate(R.layout.flow_step_one_fragment, container, false)
}
}
}
新建导航视图文件(mobile_navigation.xml)
打开导航视图文件,进行可视化编辑,包括新增Fragment,或者连接Fragment:
我们打开导航视图文件的Text标签,进入XML的编辑页面,并进行如下配置:
<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"
app:startDestination="@+id/home_dest">
<fragment
android:id="@+id/home_dest"
android:name="com.example.android.codelabs.navigation.HomeFragment"
android:label="@string/home"
tools:layout="@layout/home_fragment">
<action
android:id="@+id/next_action"
app:destination="@+id/flow_step_one_dest"
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>
<fragment
android:id="@+id/flow_step_one_dest"
android:name="com.example.android.codelabs.navigation.FlowStepFragment"
tools:layout="@layout/flow_step_one_fragment">
<argument
android:name="flowStepNumber"
app:argType="integer"
android:defaultValue="1"/>
<action
android:id="@+id/next_action"
app:destination="@+id/flow_step_two_dest">
</action>
</fragment>
<fragment
android:id="@+id/flow_step_two_dest"
android:name="com.example.android.codelabs.navigation.FlowStepFragment"
tools:layout="@layout/flow_step_two_fragment">
<argument
android:name="flowStepNumber"
app:argType="integer"
android:defaultValue="2"/>
<action
android:id="@+id/next_action"
app:popUpTo="@id/home_dest">
</action>
</fragment>
</navigation>
在导航视图文件中,我们定义了三个Fragment。每一个Fragment标签下都定义了一个Action,用来指定下一个要显示的Fragment,我们还可以为Action指定过场动画。
此外,我们需要在Activity的布局文件加入导航容器NavHostFragment:
<androidx.drawerlayout.widget.DrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.codelabs.navigation.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<fragment
android:id="@+id/my_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/mobile_navigation" />
</LinearLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
然后,我们需要在Activity中添加如下代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.navigation_activity)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.my_nav_host_fragment).navigateUp()
}
}
这里,我们重载了AppCompatActivity的onSupportNavigateUp方法。当用户点击导航栏内的向上按钮时,onSupportNavigateUp就会被调用,用来结束当前Activity,并显示父Activity。
最后,我们需要配置不同Fragment对应的跳转事件:
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val options = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
view.findViewById<Button>(R.id.navigate_destination_button)?.setOnClickListener {
findNavController().navigate(R.id.flow_step_one_dest, null, options)
}
}
这里我们为HomeFragment中的navigate_destination_button添加了一个OnClickListener,并且指定按钮点击后要跳转到flow_step_one_dest对应的Fragment(定义在mobile_navigation.xml中)。同时,我们还为这个Action定义了过场动画。
以上的实现存在一个问题,那就是我们并没有利用已经预先定义在mobile_navigation.xml中的Action。实际上,更加优雅的实现是利用Android Studio插件自动生成的Action类来实现导航,代码如下:
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.navigate_destination_button)?.setOnClickListener {
findNavController().navigate(HomeFragmentDirections.nextAction(1))
}
}
这里的HomeFragmentDirections是Android Studio插件根据mobile_navigation.xml自动生成的类,它包括了参数和过场动画的设置。直接使用HomeFragmentDirections可以避免重复定义过场动画。
我们可以用类似的方法为FlowStepFragment配置跳转事件:
class FlowStepFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<View>(R.id.next_button).setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.next_action)
)
}
}
这段代码的神奇之处就在于我们为flow_step_one_dest和flow_step_two_dest指定了相同的next_action。系统可以根据上下文,智能地推断导航目标!
可以看到,我们对于Fragment并非是通过原生的FragmentManager和FragmentTransaction进行控制。而是通过Navigation组件提供的API进行导航控制:
Navigation.findNavController(params).navigate(actionId)
Navigation.findNavController(params).navigate(action)
Navigation.createNavigateOnClickListener(actionId)
深入理解
通过上面的代码实战,我已经实现了Fragment的导航。但是,对于Navigation组件本身的设计思想和实现原理,我们并不了解。那么,接下来我们就对Navigation组件本身进行一些深入的分析。下面这张图显示了Navigation组件的架构。
NavHostFragment
NavHostFragment是对NavHost接口的实现。它是一个支持导航的Fragment容器。我们需要导航的这些Fragment都将显示在NavHostFragment上面。
NavHostFragment有两个两个重要的属性我们一定会用到:
app:defaultNavHost=“true”
app:navGraph="@navigation/nav_graph.xml"
将defaultNavHost设置为true,意味着NavHostFragment将会提供默认的导航功能。具体来说,NavHostFragment会拦截并且处理系统back键的点击事件。当系统back键被点击后,NavHostFragment不会让当前Activity退出,取而代之的是根据配置切换Fragment。
navGraph这个属性用来指定导航图XML文件。NavHostFragment会根据这个文件的配置进行导航并展示对应的Fragment。
因此,我们需要在最顶层的Activity布局文件中添加NavHostFragment,并设置defaultNavHost和 navGraph属性。
nav_graph.xml
在代码实战环节,我们已经编辑过nav_graph.xml文件了。但是其中有一个很重要的属性我们没有讲解,那就是startDestination。
app:startDestination="@id/home_dest"
startDestination申明了哪一个导航目标会被作为默认布局加载到Activity中。这也就说明了,为什么我们的例子会默认显示HomeFragment。
NavController
NavController重要职责是:
- 解析nav_graph.xml,并在内存中维护一个全局导航图
- 维护Fragment的backstack,确保用户后退时显示正确的Fragment
- 处理导航行为并显示对应的Fragment
总结
通过以上代码实战和深入分析,我们已经从实践和理论两个方面学习了Navigation组件。我强烈建议 读者们自己动手,亲自尝试使用这个组件,并把它应用到实际项目中去。