fragment 和 Navigation 组件

在“activity 和 intent”Codelab 中,您在 Words 应用中添加了 intent,以便在两个 activity 之间导航。尽管这是一种有用的导航模式,但它只是为您的应用制作动态界面的一部分。许多 Android 应用不需要每个屏幕都有单独的 activity。实际上,许多常见的界面模式(例如标签页)都存在于名为“fragment”的单个 activity 中

586ff7b88b0d2455.png

fragment 是可重复使用的界面片段;fragment 可以嵌入一个或多个 activity 中并重复使用。在上面的屏幕截图中,点按标签页不会触发用于显示下一个屏幕的 intent。切换标签页只会将上一个 fragment 替换为另一个 fragment。所有这些操作均不会启动另一个 activity。

您甚至可以在一个屏幕上同时显示多个 fragment,例如平板电脑设备的主/从布局。在下面的示例中,左侧导航界面和右侧的内容都可以包含在一个独立的 fragment 内。这两个 fragment 同时存在于同一 activity 中。

92f1ecb9aadb7797.png

如您所见,fragment 是构建高质量应用不可或缺的一部分。在此 Codelab 中,您将学习 fragment 的基础知识,并通过转换 Words 应用来使用它们。此外,您还将了解如何使用 Jetpack Navigation 组件,借助名为导航图的新资源文件在同一宿主 activity 中的 fragment 之间导航。此 Codelab 结束时,您将掌握在下一款应用中实现 fragment 所需的基本技能。

前提条件

在完成此 Codelab 之前,您应该了解下列内容:

  • 如何向 Android Studio 项目添加资源 XML 文件和 Kotlin 文件。
  • activity 生命周期大体上的工作原理。
  • 如何在现有类中替换和实现方法。
  • 如何创建 Kotlin 类的实例、访问类属性以及调用方法。
  • 基本熟悉可为 null 和不可为 null 的值,并且知道如何安全地处理 null 值。

学习内容

  • fragment 生命周期与 activity 生命周期的区别。
  • 如何将现有 activity 转换为 fragment。
  • 如何使用 Safe Args 插件向导航图添加目的地,并在 fragment 之间传递数据。

您将构建的内容

  • 您将修改 Words 应用,以使用单个 activity 和多个 fragment,并使用 Navigation 组件在 fragment 之间导航。

所需条件

  • 一台安装了 Android Studio 的计算机。
  • “activity 和 intent”Codelab 中 Words 应用的解决方案代码。

在此 Codelab 中,您将从“activity 和 intent”Codelab 结尾处的 Words 应用入手,继续进行学习。如果您已完成“activity 和 intent”Codelab,请随意使用您的代码作为起点。您也可以从 GitHub 下载到目前为止的代码。

下载此 Codelab 的起始代码

此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码可能包含您在之前的 Codelab 中已经熟悉的代码,它可能也包含您不熟悉的代码,您可以在后续 Codelab 中了解相关信息。

如果您使用 GitHub 中的起始代码,请注意文件夹名称为 android-basics-kotlin-words-app-activities。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。

fragment 就是可重复使用的应用界面片段。与 activity 类似,fragment 具有生命周期并可以响应用户输入。fragment 在屏幕上显示时,会始终包含在 activity 的视图层次结构中。由于 fragment 侧重于可重用性和模块化,因此甚至可以由单个 activity 同时托管多个 fragment。每个 fragment 都管理着自己单独的生命周期。

fragment 生命周期

和 activity 一样,fragment 可以初始化以及从内存中移除;在整个存在期间,fragment 也会在屏幕上显示、消失和重新显示。此外,与 activity 类似,fragment 也有具有多种状态的生命周期,并提供了几种可替换的方法来响应它们之间的转换。fragment 生命周期有五种状态,由 Lifecycle.State 枚举表示。

  • INITIALIZED:fragment 的一个新实例已实例化。
  • CREATED:系统已调用第一批 fragment 生命周期方法。在 fragment 处于此状态期间,系统也会创建与其关联的视图。
  • STARTED:fragment 在屏幕上可见,但没有焦点,这意味着其无法响应用户输入。
  • RESUMED:fragment 可见并已获得焦点。
  • DESTROYED:fragment 对象已解除实例化。

此外,与 activity 类似,Fragment 类还提供了多种可替换的方法来响应生命周期事件。

  • onCreate():fragment 已实例化并处于 CREATED 状态。不过,其对应的视图尚未创建。
  • onCreateView():此方法可用于膨胀布局。fragment 已进入 CREATED 状态。
  • onViewCreated():此方法在创建视图后调用。在此方法中,您通常会通过调用 findViewById() 将特定视图绑定到属性。
  • onStart():fragment 已进入 STARTED 状态。
  • onResume():fragment 已进入 RESUMED 状态,现已具有焦点(可响应用户输入)。
  • onPause():fragment 已重新进入 STARTED 状态。相应界面对用户可见。
  • onStop():fragment 已重新进入 CREATED 状态。该对象已实例化,但它在屏幕上不再显示。
  • onDestroyView():该方法在 fragment 进入 DESTROYED 状态之前调用。视图已从内存中移除,但 fragment 对象仍然存在。
  • onDestroy():fragment 进入 DESTROYED 状态。

下图总结了 fragment 生命周期以及状态之间的转换。

74470aacefa170bd.png

生命周期状态和回调方法与用于 activity 的方法非常相似。但请注意 onCreate() 方法的差异。通过 activity,您可以使用此方法膨胀布局和绑定视图。不过,在 fragment 生命周期中,系统会在创建视图之前调用 onCreate(),所以您无法在此处膨胀布局。您可以改为在 onCreateView() 中执行此操作。然后,在创建视图后,系统会调用 onViewCreated() 方法,您可以在该方法中将属性绑定到特定视图。

虽然这可能听起来理论性很强,但您已经基本了解 fragment 的工作原理以及它们与 activity 的相似之处和不同之处。在此 Codelab 的其余部分,您将实际运用这些知识。首先,您需要迁移之前使用的 Words 应用,以使用基于 fragment 的布局。然后,您可以在单个 activity 内的多个 fragment 之间实现导航。

和 activity 一样,您添加的每个 fragment 都由两个文件组成:一个用于布局的 XML 文件,以及一个用于显示数据和处理用户互动的 Kotlin 类。您将为字母列表和字词列表添加一个 fragment。

  1. 在项目导航器中选择应用,添加以下 fragment (File > New > Fragment > Fragment (Blank)),系统应为每个 fragment 生成一个类和布局文件。
  • 对于第一个 fragment,请将 Fragment Name 设置为 LetterListFragmentFragment Layout Name 应填充为 fragment_letter_list

898650e4cd0b2486.png

  • 对于第二个 fragment,请将 Fragment Name 设置为 WordListFragmentFragment Layout Name 应填充为 fragment_word_list.xml

4f04fca641487da1.png

  1. 为两个 fragment 生成的 Kotlin 类包含了很多在实现 fragment 时常用的样板代码。不过,如果您是首次学习 fragment,请直接删除这两个文件中除 LetterListFragmentWordListFragment 类声明以外的所有内容。我们将引导您从头开始实现这些 fragment,以便您了解所有代码的工作原理。删除样板代码后,Kotlin 文件应如下所示。

LetterListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class LetterListFragment : Fragment() {

}

WordListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class WordListFragment : Fragment() {

}
  1. activity_main.xml 的内容复制到 fragment_letter_list.xml 中,并将 activity_detail.xml 的内容复制到 fragment_word_list.xml 中。将 fragment_letter_list.xml 中的 tools:context 更新为 .LetterListFragment,并将 fragment_word_list.xml 中的 tools:context 更新为 .WordListFragment

完成更改后,fragment 布局文件应如下所示。

fragment_letter_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp" />

</FrameLayout>

fragment_word_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp"
       tools:listitem="@layout/item_view" />

</FrameLayout>

与 activity 一样,您需要膨胀布局并绑定各个视图。使用 fragment 生命周期时,只存在一些细微差别。我们将引导您完成设置 LetterListFragment 的流程,然后您将有机会对 WordListFragment 执行相同的操作。

如需在 LetterListFragment 中实现视图绑定,您首先需要获取对 FragmentLetterListBinding 的可为 null 的引用。在 build.gradle 文件的 buildFeatures 部分下启用 viewBinding 属性后,Android Studio 会为每个布局文件生成这样的绑定类。您只需要在 fragment 类中为 FragmentLetterListBinding 内的每个视图分配属性即可。

类型应为 FragmentLetterListBinding?,其初始值应为 null。为何要将它设置为可为 null?由于在调用 onCreateView() 之前,您无法膨胀布局。在 LetterListFragment 的实例创建(其生命周期以 onCreate() 开头)之后,要等待一段时间,此属性才会实际可用。另请注意,fragment 的视图可以在 fragment 的整个生命周期内多次创建和销毁。因此,您还需要在另一个生命周期方法 onDestroyView() 中重置该值。

  1. LetterListFragment.kt 中,首先获取对 FragmentLetterListBinding 的引用,并将引用命名为 _binding
private var _binding: FragmentLetterListBinding? = null

由于它可为 null,因此,每当您访问 _binding 的属性(例如 _binding?.someView)时,您都需要添加 ? 以确保 null 安全。不过,这并不意味着您仅仅因为一个 null 值就必须在代码中散放英文问号。如果您确定某个值在您访问时不会为 null,则可以在其类型名称后面加上 !!。然后,您可以像访问任何其他属性一样访问它,而不使用 ? 运算符。

  1. 创建一个名为 binding(不加下划线)的新属性,并将其设置为 _binding!!
private val binding get() = _binding!!

在这里,get() 表示此属性仅供获取。也就是说,您可以获取该值,但是,该值一旦进行分配之后(如此处所示),就不能再分配给其他属性。

  1. 如需实现 onCreate(),只需调用 setHasOptionsMenu() 即可。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setHasOptionsMenu(true)
}
  1. 请记住,使用 fragment 时,布局会在 onCreateView() 中膨胀。通过膨胀视图、设置 _binding 的值并返回根视图,可实现 onCreateView()
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}
  1. binding 属性下,为 recycler 视图创建一个属性。
private lateinit var recyclerView: RecyclerView
  1. 然后,在 onViewCreated() 中设置 recyclerView 属性的值,并调用 chooseLayout(),就像在 MainActivity 中一样。您很快就会将 chooseLayout() 方法移至 LetterListFragment 中,所以不用担心会出错。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   chooseLayout()
}

请注意,绑定类已为 recyclerView 创建一个属性,您不需要为每个视图调用 findViewById()

  1. 最后,在 onDestroyView() 中,将 _binding 属性重置为 null,因为相应视图已不存在。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. 唯一需要注意的一点是,在使用 fragment 时,onCreateOptionsMenu() 方法存在一些细微差别。虽然 Activity 类具有名为 menuInflater 的全局属性,但 fragment 没有此属性。菜单膨胀器会转而传入 onCreateOptionsMenu() 中。另请注意,用于 fragment 的 onCreateOptionsMenu() 方法不需要返回语句。按如下所示实现该方法:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.layout_menu, menu)

   val layoutButton = menu.findItem(R.id.action_switch_layout)
   setIcon(layoutButton)
}
  1. chooseLayout()setIcon()onOptionsItemSelected() 剩下的代码按原样从 MainActivity 中移出。唯一需要注意的区别是,由于与 activity 不同,fragment 不是 Context。不能传入 this(引用 fragment 对象)作为布局管理器的上下文。不过,fragment 提供了 context 属性,您可以改用此属性。其余代码与 MainActivity 相同。
private fun chooseLayout() {
   when (isLinearLayoutManager) {
       true -> {
           recyclerView.layoutManager = LinearLayoutManager(context)
           recyclerView.adapter = LetterAdapter()
       }
       false -> {
           recyclerView.layoutManager = GridLayoutManager(context, 4)
           recyclerView.adapter = LetterAdapter()
       }
   }
}

private fun setIcon(menuItem: MenuItem?) {
   if (menuItem == null)
       return

   menuItem.icon =
       if (isLinearLayoutManager)
           ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
       else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           isLinearLayoutManager = !isLinearLayoutManager
           chooseLayout()
           setIcon(item)

           return true
       }

       else -> super.onOptionsItemSelected(item)
   }
}
  1. 最后,从 MainActivity 复制 isLinearLayoutManager 属性。将此属性放在 recyclerView 属性声明的正下方。
private var isLinearLayoutManager = true
  1. 现在,所有功能都已迁移至 LetterListFragment,因此所有 MainActivity 类都需要膨胀布局,使 fragment 在视图中显示。请直接删除 MainActivity 中除 onCreate() 以外的所有内容。更改后,MainActivity 应仅包含以下内容。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

轮到您了

这就是将 MainActivity 迁移到 LettersListFragment 的过程。迁移 DetailActivity 的过程几乎完全相同。请执行以下步骤,将代码迁移到 WordListFragment

  1. 将伴生对象从 DetailActivity 复制到 WordListFragment。确保对 WordAdapter 中的 SEARCH_PREFIX 的引用已更新为引用 WordListFragment
  2. 添加一个 _binding 变量,该变量应该可为 null,并且初始值为 null
  3. 添加一个名为 binding 的仅供获取变量,等效于 _binding 变量。
  4. 膨胀 onCreateView() 中的布局,同时设置 _binding 的值并返回根视图。
  5. onViewCreated() 中执行其余的所有设置:获取对 recycler 视图的引用,设置其布局管理器和适配器,并添加其项目装饰。您需要从 intent 中获取字母。由于 fragment 没有 intent 属性,并且通常不应访问父 activity 的 intent,目前,您需要引用 activity.intent(而不是 DetailActivity 中的 intent)来获取 extra。
  6. onDestroyView 中,将 _binding 重置为 null。
  7. 删除 DetailActivity 中剩余的代码,只保留 onCreate() 方法。

请尝试自行逐步完成相关步骤,然后再继续。详细演示介绍了下一步。

希望您能享受将 DetailActivity 迁移至 WordListFragment 的机会。这与将 MainActivity 迁移到 LetterListFragment 几乎完全相同。如果您在任何时候遇到困难,请参阅下文概述的步骤。

  1. 首先,将伴生对象复制到 WordListFragment
companion object {
   val LETTER = "letter"
   val SEARCH_PREFIX = "https://www.google.com/search?q="
}
  1. 然后,在 LetterAdapter 内用于执行 intent 的 onClickListener() 中,您需要更新对 putExtra() 的调用,也就是将 DetailActivity.LETTER 替换为 WordListFragment.LETTER
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
  1. 同样,在 WordAdapter 中,您需要更新用于导航到相应字词搜索结果的 onClickListener(),也就是将 DetailActivity.SEARCH_PREFIX 替换为 WordListFragment.SEARCH_PREFIX
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
  1. 回到 WordListFragment 中,添加 FragmentWordListBinding? 类型的绑定变量。
private var _binding: FragmentWordListBinding? = null
  1. 然后,您可以创建一个仅供获取变量,这样无需使用 ? 即可引用视图。
private val binding get() = _binding!!
  1. 然后膨胀布局,同时分配 _binding 变量并返回根视图。请记住,对于 fragment,您需要在 onCreateView()(而非 onCreate())中执行此操作。
override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}
  1. 接下来,您需要实现 onViewCreated()。这与在 DetailActivityonCreateView() 中配置 recyclerView 几乎完全相同。不过,由于 fragment 无法直接访问该 intent,因此您需要使用 activity.intent 引用它。但是,您必须在 onCreateView() 中执行此操作,因为无法保证该 activity 在生命周期的早期就存在。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}
  1. 最后,您可以在 onDestroyView() 中重置 _binding 变量。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. 在所有这些功能移至 WordListFragment 后,您现在可以从 DetailActivity 中删除此代码。剩下的应该就是 onCreate() 方法。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityDetailBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

移除 DetailActivity

现在,您已成功将 DetailActivity 的功能迁移到 WordListFragment 中,无需再使用 DetailActivity。您可以直接删除 DetailActivity.ktactivity_detail.xml,还可以对清单稍作更改。

  1. 首先,删除 DetailActivity.kt

dd3b0bcf3ec81c9.png

  1. 确保取消选中 Safe delete,然后点击 OK

f2f1ff137b0057a7.png

  1. 然后,删除 activity_detail.xml。同样,确保取消选中 Safe delete

6090c1d640433e07.png

  1. 最后,由于 DetailActivity 不再存在,请从 AndroidManifest.xml 中移除以下内容。
<activity
   android:name=".DetailActivity"
   android:parentActivityName=".MainActivity" />

删除 DetailActivity 后,您将得到两个 fragment(LetterListFragment 和 WordListFragment)以及一个 activity (MainActivity)。在下一部分,您将学习 Jetpack Navigation 组件,还将修改 activity_main.xml,使其可以在 fragment 之间显示和导航,而不是托管静态布局。

Android Jetpack 提供了 Navigation 组件,可帮助您在应用中处理任何简单或复杂的导航实现。Navigation 组件有三个关键部分,可供您在 Words 应用中实现导航。

  • 导航图:导航图是一种 XML 文件,可以直观地呈现应用内的导航。该文件由若干目的地组成,对应于各个 activity 和 fragment,以及它们之间可通过代码用来从一个目的地导航到另一个目的地的操作。与布局文件一样,Android Studio 提供了一个可视化编辑器,用于向导航图添加目的地和操作。
  • NavHostNavHost 用于在 activity 内显示导航图中的目的地。当您在 fragment 之间导航时,NavHost 中显示的目的地会相应更新。您将在 MainActivity 中使用名为 NavHostFragment 的内置实现。
  • NavController:您可以使用 NavController 对象控制 NavHost 中显示的目的地之间的导航。在使用 intent 时,您必须通过调用 startActivity 导航到新屏幕。借助 Navigation 组件,您可以通过调用 NavControllernavigate() 方法来交换显示的 fragment。NavController 还可帮您处理常见任务,例如响应系统“向上”按钮可回到之前显示的 fragment。
  1. 在项目级 build.gradle 文件的 buildscript > ext 中,在 material_version 下,将 nav_version 设置为 2.3.1
buildscript {
    ext {
        appcompat_version = "1.2.0"
        constraintlayout_version = "2.0.2"
        core_ktx_version = "1.3.2"
        kotlin_version = "1.3.72"
        material_version = "1.2.1"
        nav_version = "2.3.1"
    }

    ...

}

  1. 在应用级 build.gradle 文件中,将以下内容添加到依赖项组中。
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Safe Args 插件

首次在 Words 应用中实现导航时,您在两个 activity 之间使用了显式 intent。为了在两个 activity 之间传递数据,您调用了 putExtra() 方法,同时传递了所选的字母。

开始在 Words 应用中实现 Navigation 组件之前,您还需要添加一个名为 Safe Args 的 Gradle 插件;在 fragment 之间传递数据时,该插件可帮助您确保类型安全。

执行以下步骤可将 SafeArgs 集成到您的项目中。

  1. 在顶级 build.gradle 文件的 buildscript > dependencies 中,添加以下类路径。
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
  1. 在应用级 build.gradle 文件中,在顶部的 plugins 中添加 androidx.navigation.safeargs.kotlin
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'androidx.navigation.safeargs.kotlin'
}
  1. 在修改 Gradle 文件后,您可能会在顶部看到一个黄色的横幅,要求您同步项目。点击 Sync Now 并等待一两分钟的时间,此时 Gradle 会更新项目依赖项,以反映您所做的更改。

854d44a6f7c4c080.png

同步完成后,您可以继续下一步:添加导航图。

现在,您已基本了解 fragment 及其生命周期,接下来可以做些更有趣的事情了。下一步是整合 Navigation 组件。Navigation 组件指的就是用于实现导航的工具集合,尤其是在 fragment 之间。您将使用新的可视化编辑器在 fragment 之间实现导航;这种新的可视化编辑器就是导航图(简称 NavGraph)。

什么是导航图?

导航图(简称 NavGraph)是应用导航的虚拟映射。每个屏幕(对您而言,就是每个 fragment)都是可能的导航“目的地”。NavGraph 可以用显示各目的地之间关系的 XML 文件来表示。

在后台,它实际上会创建 NavGraph 类的新实例。不过,导航图中的目的地由 FragmentContainerView 向用户显示。您只需创建一个 XML 文件并指定可能的目的地即可。然后,您可以使用生成的代码在 fragment 之间导航。

在 MainActivity 中使用 FragmentContainerView

由于您的布局现在包含在 fragment_letter_list.xmlfragment_word_list.xml 中,因此您的 activity_main.xml 文件不再需要包含应用中第一个屏幕的布局。取而代之的是,您将改变 MainActivity 的用途,使其包含一个 FragmentContainerView,充当您的 fragment 的 NavHost。从这时开始,应用中的所有导航都将在 FragmentContainerView 内进行。

  1. 替换 activity_main.xmlFrameLayout 的内容,也就是将 androidx.recyclerview.widget.RecyclerView 替换为 FragmentContainerView。将 ID 指定为 nav_host_fragment,并将其高度和宽度设置为 match_parent 以填充整个框架布局。

将下面的代码:

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        ...
        android:padding="16dp" />

替换为此代码:

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. 在 id 属性下,添加 name 属性并将其设置为 androidx.navigation.fragment.NavHostFragment。虽然您可以为此属性指定特定的 fragment,但将其设置为 NavHostFragment 可让 FragmentContainerView 在 fragment 之间导航。
android:name="androidx.navigation.fragment.NavHostFragment"
  1. 在 layout_height 和 layout_width 属性下方,添加名为 app:navHost 的属性,并将其设置为 "true"。这样一来,fragment 容器就可以与导航层次结构进行交互了。例如,如果您按下系统返回按钮,容器将回到之前显示的 fragment,就像呈现新 activity 时发生的情况一样。
app:defaultNavHost="true"
  1. 添加一个名为 app:navGraph 的属性,并将其设置为 "@navigation/nav_graph"。这将指向用于定义应用的 fragment 如何导航到另一个 fragment 的 XML 文件。目前,Android Studio 会显示未解析的符号错误。您将在下一任务中解决此问题。
app:navGraph="@navigation/nav_graph"
  1. 最后,由于您使用应用命名空间添加了两个属性,因此请务必向 FrameLayout 添加 xmlns:app 属性。
<xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

以上就是在 activity_main.xml 中进行的所有更改。接下来,您需要创建 nav_graph 文件。

设置导航图

添加导航图文件 (File > New > Android Resource File),然后按照如下所示填写相应字段。

  • File name:nav_graph.xml.这与您为 app:navGraph 属性设置的名称相同。
  • Resource type:Navigation。然后,Directory name 应该会自动更改为“navigation”。系统将会创建一个名为“navigation”的新资源文件夹。

e26ed91764a5616e.png

该 XML 文件一旦创建完毕,您就会看到一个新的可视化编辑器。由于您已在 FragmentContainerViewnavGraph 属性中引用了 nav_graph,因此,如需添加新的目的地,请点击屏幕左上方的新建按钮,为每个 fragment 创建一个目的地(一个用于 fragment_letter_list,一个用于 fragment_word_list)。

307d036fce790feb.gif

添加后,这些 fragment 应会出现在屏幕中间的导航图上。您还可以使用左侧的组件树选择特定的目的地。

创建导航操作

如需创建 letterListFragmentwordListFragment 目的地之间的导航操作,请将鼠标悬停在 letterListFragment 目的地上,然后从右侧出现的圆圈拖动到 wordListFragment 目的地处。

c9477af5828a83f4.gif

现在,您应该看到已经创建了一个箭头来表示两个目的地之间的操作。点击箭头,您可以在“Attributes”窗格中看到此操作的名称为 action_letterListFragment_to_wordListFragment,可在代码中引用。

指定 WordListFragment 的参数

使用 intent 在 activity 之间导航时,您指定了“extra”,以便将选定字母传递给 wordListFragment。Navigation 还支持在目的地之间传递参数,另外可以用确保类型安全的方式执行此操作。

选择 wordListFragment 目的地,然后在“Attributes”窗格的 Arguments 下,点击加号按钮以创建一个新参数。

该参数的名称应为 letter,类型应为 String。这就是您之前添加的 Safe Args 插件的用武之地。将此参数指定为字符串可以确保在代码中执行导航操作时,预计会有一个 String

b6bc3eaacd14bf50.png

设置起始目的地

当您的 NavGraph 知道所有需要的目的地时,FragmentContainerView 如何知道首先要显示哪个 fragment?在 NavGraph 中,您需要将字母列表设置为起始目的地。

设置起始目的地,方法是选择 letterListFragment 并点击 Assign start destination 按钮。

99bb085e39dd7b4a.png

这些就是目前您需要使用 NavGraph 编辑器完成的一切操作。现在,请构建项目。这会根据您的导航图生成一些代码,以便您使用刚创建的导航操作。

执行导航操作

打开 LetterAdapter.kt 来执行导航操作。这只涉及两个步骤。

  1. 删除按钮的 onClickListener() 的内容。与之相对,您需要检索刚刚创建的导航操作。将以下内容添加到 onClickListener() 中。
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())

您可能无法识别其中一些类和函数的名称,这是因为它们是在您构建项目后自动生成的。这就是您在第一步中添加的 Safe Args 插件的用武之地 - 在 NavGraph 上创建的操作会变为您可以使用的代码。不过,名称应该相当直观。LetterListFragmentDirections 表示从 letterListFragment 开始的所有可能的导航路径。函数 actionLetterListFragmentToWordListFragment()

是导航到 wordListFragment. 的特定操作。

对导航操作进行引用后,只需获取对 NavController(一个用于执行导航操作的对象)的引用,并调用 navigate(),同时传入相应操作即可。

holder.view.findNavController().navigate(action)

配置 MainActivity

最后一项设置在 MainActivity 中进行。您只需在 MainActivity 中完成几项更改,即可让一切正常运行。

  1. 创建 navController 属性。它将标记为 lateinit,因为它会在 onCreate 中设置。
private lateinit var navController: NavController
  1. 然后,在 onCreate() 中调用 setContentView() 后,引用 nav_host_fragment(这是 FragmentContainerView 的 ID),并将其分配给您的 navController 属性。
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
  1. 然后,在 onCreate() 中调用 setupActionBarWithNavController(),同时传入 navController。这样可确保操作栏(应用栏)按钮(例如 LetterListFragment 中的菜单选项)可见。
setupActionBarWithNavController(navController)
  1. 最后,实现 onSupportNavigateUp()。除了在 XML 中将 defaultNavHost 设置为 true,此方法还支持您处理向上按钮。不过,您的 activity 需要提供相应实现。
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}

此时,所有组件就已准备就绪,支持用户使用 fragment 进行导航。不过,由于现在使用 fragment 而不是 intent 来执行导航,因此您在 WordListFragment 中使用的字母的 intent extra 将不再有效。在下一步中,您将通过更新 WordListFragment 来获取 letter 参数。

之前,您在 WordListFragment 中引用 activity?.intent 即可访问 letter extra。这种做法虽然行之有效,但并非最佳做法,因为 fragment 可以嵌入其他布局中,而在大型应用中,假定 fragment 属于哪一个 activity 要难得多。此外,使用 nav_graph 执行导航且使用安全参数时是没有 intent 的,因此尝试访问 intent extra 根本就行不通。

幸运的是,访问安全参数相当简单,您无需等待 onViewCreated() 调用完毕。

  1. WordListFragment 中,创建一个 letterId 属性。您可以将此属性标记为 lateinit,这样您就不必将它设置为可为 null 了。
private lateinit var letterId: String
  1. 然后替换 onCreate()(不是 onCreateView()onViewCreated()!),并添加以下内容。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}

由于 arguments 有可能是可选的,请注意您调用了 let() 并传入了 lambda。此代码将执行,并假设 arguments 不为 null,同时传入 it 参数的非 null 参数。不过,如果 argumentsnull,该 lambda 不会执行。

96a6a3253cea35b0.png

虽然这不是实际代码的一部分,但 Android Studio 提供了一条有用的提示,以便您了解 it 参数。

到底什么是 Bundle?您可将其看作用于在类之间传递数据的键值对,例如 activity 和 fragment。实际上,当您在此应用的第一个版本中执行 intent 时,调用 intent?.extras?.getString() 时使用的就是 bundle。使用 fragment 从参数中获取字符串的方式完全相同。

  1. 最后,您可以在设置 recycler 视图的适配器时访问 letterId。将 onViewCreated() 中的 activity?.intent?.extras?.getString(LETTER).toString() 替换为 letterId
recyclerView.adapter = WordAdapter(letterId, requireContext())

大功告成!花一点时间运行您的应用。现在,您可以在一个 activity 中于两个屏幕之间导航,而不必使用任何 intent。

您已成功将两个屏幕转换为使用 fragment。在进行任何更改之前,每个 fragment 的应用栏均具有针对应用栏中每个 activity 的描述性标题。不过,在转换为使用 fragment 之后,详情 activity 中不会再有此标题。

c385595994ba91b5.png

fragment 具有名为 "label" 的属性,您可通过该属性设置父 activity 将在应用栏中使用的标题。

  1. strings.xml 中的应用名称后面,添加以下常量。
<string name="word_list_fragment_label">Words That Start With {letter}</string>
  1. 您可以在导航图中为每个 fragment 设置标签。返回 nav_graph.xml,选择组件树中的 letterListFragment,然后在“Attributes”窗格中将标签设置为 app_name 字符串

4568d78c606999d.png

  1. 选择 wordListFragment 并将标签设置为 word_list_fragment_label

7e7e55ea2dfb65bb.png

恭喜您坚持到这里!再次运行应用,您应该能看到所有内容,就像此 Codelab 的开头描述的那样,现在,您的所有导航均托管在单个 activity 中,并且每个屏幕都使用单独的 fragment。

此 Codelab 的解决方案代码可在下面显示的项目中找到。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。
  • fragment 是可以嵌入到 activity 中且可重复使用的界面片段。
  • fragment 的生命周期与 activity 的生命周期不同,前者的视图在 onViewCreated()(而非 onCreateView())中设置。
  • FragmentContainerView 用于在其他 activity 中嵌入 fragment,并可以管理 fragment 之间的导航。

使用 Navigation 组件

  • 通过设置 FragmentContainerViewnavGraph 属性,您可以在一个 activity 内的多个 fragment 之间导航。
  • 您可以使用 NavGraph 编辑器添加导航操作,还可以指定不同目的地之间的参数。
  • 虽然使用 intent 进行导航需要您传入 extra,但 Navigation 组件使用 SafeArgs 为导航操作自动生成类和方法,从而确保参数实现类型安全。

fragment 的用例。

  • 借助 Navigation 组件,许多应用可在单个 activity 中管理其整个布局,且所有导航都在 fragment 之间完成。
  • fragment 可实现常见的布局模式,如平板电脑上的主/从布局,或同一 activity 中的多个标签页。