使用导航组件: 对话框目的地 | MAD Skills

这是一个新的系列文章,我们称之为 “Modern Android Development 技巧”,简称为 “MAD Skills”。本系列文章致力于帮助开发者们打造更好的现代 Android 开发体验,敬请关注。

今天为大家发布本系列文章中的第二篇: 导航到对话框目的地,如果您想了解第一篇发布的内容,请点击这里查看本系列的第一篇: 导航组件概览

概览

在本系列的 上一篇文章 中,我大致介绍了导航组件以及如何使用导航图。

在这篇文章中,我会介绍如何使用 API 来导航到对话框目的地 (dialog destination)。大部分的导航发生在 Fragment 目的地之间,在 UI 中的 NavHostFragment 对象内部,fragment 会被替换出去。但其实导航到容器外的目的地包括对话框也是可行的。就像我们实现普通的目的地一样,我们也可以使用导航图来实现导航到对话框目的地。

甜甜圈记录应用

我有一个小麻烦: 我超爱甜甜圈。

我希望能记得之前吃的哪些甜甜圈是好吃的,这样下次我就可以再买它们。而对于那些我不喜欢的,我也可以避免再买到它们。但我很健忘,所以问题来了,我如何才能记录如此重要的数据呢?

我知道了: 我要用一个应用!

可惜的是,我竟然在 Play 商店中找不到一个甜甜圈记录的应用 (太不可思议了)。所以我只能自己写一个应用。这个应用会有一个我所有吃过甜甜圈的列表,也包括我记录下的关于它们每一个的信息,比如名字、介绍、或许还有一张照片以及相应的评分。

这将是一个相当简单的应用,它包括两个页面:

  • 一个甜甜圈列表页
  • 一个可以输入甜甜圈相关信息的表单页,它既可以是关于我要新增到列表中的甜甜圈,也可以是关于我要编辑的已存在列表中的甜甜圈

至于信息编辑页面,我希望能用一个对话框。我想实现在当前 activity 上弹出一个相对轻量级的弹窗,而不是替换掉整个页面。我知道导航组件可以处理目的地,但是那只能替换掉单个 NavHostFragment 中的 fragment,对吗?

对,也不对。导航组件默认的行为确实是替换掉 NavHostFragment 中的 fragment。但是导航组件同样可以处理在 NavHostFragment 之外的对话框目的地。

通过模版创建一个工程

首先,我会展示如何在一个新应用中设定导航的基本元素。然后,我会展示我已经写好的甜甜圈记录应用,这样您可以大致了解这将是一个怎样的应用。(我叫这个为 Julia Child 技巧。在她多年前的烹饪节目中,Child 女士会先介绍菜谱,紧接着快速地展示完成的菜品,最后才是准备工作以及烹饪等中间冗长乏味的部分)

从 Android Studio 3.6 以后,您可以选择任一新建工程模版来使用导航组件。我发现这样做很方便,即便我最终的界面跟模版应用根本不像,至少模版会帮我处理类似下载合适的依赖,以及创建基础代码和资源等工作。

一开始我们需要在 Android Studio 中创建一个 Basic Activity。这一步我在 上一篇文章 中都介绍过,您可以查阅并获取更多详细信息。这里我们将直接跳到下一步。

对话框目的地

如果注意观察导航图中我们新建的 basic activity,您会发现应用此时有两个目的地,同时也包括了在它们彼此之间跳转的操作 (action)。这两个目的地都是 fragment,模版帮我们实现了在 NavHostFragment 内部替换它们的操作。

Basic Activity 附带两个 fragment 以及在它们之间导航的操作

这基本上就是所有我们需要的,所差的是我们需要目的地是一个我们可以输入甜甜圈详细信息的对话框。为了创建这个目的地,首先我们创建所需要的对话框类。

首先,我们在 UI 中创建一个带文本占位符的布局。在布局资源文件夹下创建一个名为 my_dialog.xml 的文件。然后在这个布局中,添加一个 TextView 并且限制它的四边边距使其保持在容器的正中间。结果应该看起来像下图:

image

我们创建的简单对话框,包括一个居中的文本占位符

接下来,创建一个 Fragment 用来加载上面创建的布局。在 main 包中创建一个新的 Kotlin 文件并命名为 MyDialog.kt。在该文件中,创建一个继承自 BottomSheetDialogFragment 的子类 MyDialog,并且重写 onCreateView() 以返回一个加载自我们刚刚创建的布局的视图。

class MyDialog : BottomSheetDialogFragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.my_dialog, 
            container, false)
    }
}

我们已经得到了对话框 fragment,现在可以创建一个可以导航到它的目的地。让我们回到导航图并新增一个目的地。在弹出的菜单中,您应该可以发现系统已经识别出 MyDialog,选中它。

image

选中列表中的 MyDialog 作为新的目的地,并且确保它是一个 “对话框” 而不是一个 “Fragment”

善于观察的读者可能会从上面截图中发现一个 IDE 的小 bug。尽管 MyDialog 事实上是一个 Dialog 对象,导航工具有时候不能准确地识别出来,而把它添加为一个 Fragment 目的地。这个结果绝不是我们所期望的。虽然它并不是经常发生 (好吧,出现了不可预期的结果),但是在我开发这个示例的过程中已经出现了多次这个问题,所以在这里我希望强调一下。它确实很容易让人迷惑。还好,解决方法也非常简单,所以大家这里只需要知道有可能会出现这样的问题就可以了。

如果您碰到了这个问题,直接去导航图的 XML 代码中将 fragment 标签改成 dialog,就可以解决这个问题。这是我解决这个问题之后的代码:

<dialog
    android:id=”@+id/myDialog”
    android:name=”com.android.samples.navdialogsample.MyDialog”
    android:label=”MyDialog” />

另外,我已经就这个问题咨询了 Android Studio 团队。据说这个问题是由于内部依赖搜索的顺序导致的。他们正在修复这个问题。

对话框的目的地现在已经准备好了,接下来我们可以创建一个从主界面跳转到对话框目的地的操作:

创建一个新的从 FirstFragment 导航到对话框的操作

我们还需要额外的一步才可能导航到这个对话框。在 FirstFragment 的代码中,有一段代码 (Basic Activity 模版自动创建的) 处理了按钮点击事件并导航到 SecondFragment 目的地:

view.findViewById<Button>(R.id.button_first).setOnClickListener {
    findNavController().navigate(
        R.id.action_FirstFragment_to_SecondFragment)
}

我们只需使用适当的 id 将导航目的地改变为对话框,这里的 id 正是在导航图中创建目的地时所生成的。

view.findViewById<Button>(R.id.button_first).setOnClickListener {
    findNavController().navigate(
        R.id.action_FirstFragment_to_myDialog)
}

大功告成!终于可以运行我们的应用来看看实际效果了。当我们点击按钮的时候,它会如期地带我们去那个对话框目的地。

点击按钮会打开一个非常矮小的带有文本占位符的对话框

您可能注意到对话框显示的尺寸要远比它在设计工具中看起来小得多 — 这是因为这个对话框的内容只有那个 TextView 占位符作为内容。但请相信我,那就是我们的对话框。

我们刚创建的其实是我想要的甜甜圈记录应用的一个相对简化的版本,只是想通过它来展示如何创建以及使用对话框作为目的地的基本步骤。接下来,让我们看一下甜甜圈应用的实际代码。

DonutTracker 应用实践

“剧透” 警告: 我已经写完了 DonutTracker 应用。我会带您浏览关键的实现步骤,大家可以看到我是如何使用对话框目的地导航的。

首先,这是应用的导航图:

image

在 DonutTracker 的导航图中有两个目的地

您会发现主页目的地依然存在,只不过叫做 donutList。这是那个包含甜甜圈列表 (使用 RecyclerView) 的 fragment。我还创建了第二个目的地,叫做 donutEntryDialogFragment,这个是用来让用户编辑甜甜圈信息的。

如果我们查看 DonutList 的代码,该 fragment 包含了那个展示列表数据的 RecyclerView,我们可以发现导航是如何被处理的。点击 FloatingActionButton (FAB) 按钮触发了导航到对话框:

binding.fab.setOnClickListener { fabView ->
    fabView.findNavController().navigate(DonutListDirections
        .actionDonutListToDonutEntryDialogFragment())
}

注意我这里用的是 视图绑定 来获取 FloatingActionButton 的引用,也即 binding.fab 的引用。

除此之外,我们同样可以在这个文件中看到点击 RecyclerView 中的列表项是如何导航到编辑那一项的对话框的:

donut ->
    findNavController().navigate(DonutListDirections
        .actionDonutListToDonutEntryDialogFragment(donut.id))

关于上述代码片段,有几点需要注意:

首先,我们在此使用的 navigate() 函数 (使用 Directions 对象导航) 的语法和之前通过 Basic Activity 模版创建的 (导航到一个通过 R.id.action_FirstFragment_to_myDialog 指定的操作) 略有不同。这是因为上述代码片段来自于 DonutTracker 应用的最终版本,在该版本中我使用了 SafeArgs。SafeArgs 可以生成 Directions 代码,这使得目的地之间带有参数传递的跳转实现起来更加容易。

其次,我们从 FAB 导航时 (不需要传递参数给 Directions 对象) 调用 navigate() 方法和从甜甜圈列表中任一列表项导航时 (需要传递 donut.id) 不太一样。这个区别可以让我们决定究竟是创建一个新甜甜圈 (当没有传递参数) 还是编辑已有的甜甜圈 (当传递了 donut.id)。(剧透警告: 我会在接下来的文章中介绍这一主题,您也可以同时查阅 完整代码。)

运行该应用展示了它是如何工作的。如您所见,我已经预先在应用中输入了一些重要的甜甜圈数据:

DonutTracker 应用展示着一个诱人的甜甜圈列表

点击 FAB,我们可以看到一个待输入新甜甜圈信息的对话框:

点击 FAB 导航到输入新甜甜圈信息的对话框目的地

如果我们点击任一已存在的甜甜圈 (这里我点击了 “fundonut”,因为很显然这里的描述需要再润色一下),应用会带我们到同一个对话框目的地,在这里我们可以编辑刚刚点击的甜甜圈的信息。

点击任一甜甜圈会导航到编辑其信息的对话框

点击 DONE 按钮,将保存更改到数据库中并且返回更新的列表;而点击 CANCEL 按钮,将放弃掉所有的编辑并返回。注意: 点击返回按钮,同样会返回甜甜圈列表,因为导航组件已经自动为我们设置好了返回栈。

总结

通过这篇文章我们了解了如何使用内置的导航组件快速地创建一个新应用,并且学习了如何导航到对话框目的地。在接下来的文章中,我们会继续通过开发这个应用为大家展示导航组件的其它功能,当然也同时会实现一个功能更加强大的甜甜圈记录应用。

更多信息

更多关于导航组件的详情,请查看 导航组件使用入门文档

DonutTracker 应用的完整代码,请查看 Github 示例

更多 MAD Skills 系列内容,请查看 Android Developers 频道