对话框

对话框:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“对话框”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“掌握 AlertDialog 构建模式、DialogFragment 的配置变更安全机制、以及 FragmentResult API 的双向数据传递。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用双视口、大字体、焦点顺序、状态语义和对比度截图独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么需要对话框?

你在用 App 时肯定见过这些场景:弹出一个窗口问"确定删除吗?"、选个日期、输个名字。这些都是对话框——一种悬浮在主界面之上的轻量级 UI。

对话框的核心价值:打断用户当前操作,强制做出一个选择后再继续。它不是"顺便问问你"的那种弱提醒(那是 Snackbar/Toast 的活),而是"你不回答我就不让你往下走"的强交互。

用得好,对话框让交互清晰且安全(删除前确认)。用不好,对话框满天飞就是用户体验灾难——所以 Android 提供了一套规范的对话框 API,把常见的模式(确认、输入、选择、日期)都帮你封装好了。

对话框是怎么工作的

AlertDialog:最通用的对话框

使用构建器模式——你先用 AlertDialog.Builder 一点点搭好对话框的样子(标题、内容、按钮),最后 build() 创建它。三个区域的按钮分工明确:

  • Positive 按钮(确定/保存)——肯定操作,通常在右侧
  • Negative 按钮(取消)——否定操作,Positive 左边
  • Neutral 按钮(稍后提醒)——中立操作,最左边

DialogFragment:配置变更的安全网

为什么不能直接在 Activity 里 AlertDialog.Builder(...).show()

问题出在屏幕旋转。当你旋转手机时,Activity 被销毁重建——但那个正在显示的对话框不属于 Activity 生命周期管理范围,重建后对话框消失了,用户之前的选择也丢了。DialogFragment 解决了这个问题:它本身是一个 Fragment,受 FragmentManager 管理,旋转屏幕时跟普通 Fragment 一样被保存和恢复。

  • 就是一个"包着对话框的 Fragment"——对话框的显示和隐藏跟 DialogFragment 的生命周期绑定,配置变更时自动保存恢复,你不需要做任何额外操作。

FragmentResult API:对话框怎么把结果传回来

对话框最常见的需求就是"用户选完了之后,发起方要知道选了什么"。传统做法用 setTargetFragment()(现已废弃)——发起方把 Fragment 设为目标,接收方通过 onActivityResult 接收。这套方案的耦合度太高了。

现代的做法是 FragmentResult API——可以理解为"广播机制":

  • 对话框里用 setFragmentResult(requestKey, bundle) 发出结果
  • 发起方用 setFragmentResultListener(requestKey) 监听结果
  • 双方通过 requestKey(一个字符串)配对——发的人和收的人用同一个 key,就跟对讲机共用同一个频道一样
// 发起方 (CrimeDetailFragment)
setFragmentResultListener("requestKey") { _, bundle ->
    val result = bundle.getString("selectedDate")
}
 
// 对话框方 (DatePickerFragment) 在用户选完日期后
setFragmentResult("requestKey", bundleOf("selectedDate" to date.toString()))
分步1 / 4

① 创建 DialogFragment 子类

新建一个继承 DialogFragment() 的类。它本身是一个 Fragment——受 FragmentManager 管理,旋转屏幕时自动保存恢复,不会像普通 Dialog 那样丢失。重写 onCreateDialog() 方法返回要显示的 Dialog 对象。

代码逐段拆解

第一步:创建带 DatePicker 的 DialogFragment

import { Objectives } from "@/components/mdx/objectives";
import { Term } from "@/components/mdx/term";
import { Callout } from "@/components/mdx/callout";
import { Step, Stepper } from "@/components/mdx/stepper";
import { Answer, Exercises } from "@/components/mdx/exercises";
import { Glossary, GlossaryItem } from "@/components/mdx/glossary";
import { Attribution } from "@/components/mdx/attribution";
import androidx.fragment.app.DialogFragment
import android.app.DatePickerDialog
import java.util.Calendar
 
class DatePickerFragment : DialogFragment() {
 
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val calendar = Calendar.getInstance()
        val year = calendar.get(Calendar.YEAR)
        val month = calendar.get(Calendar.MONTH)
        val day = calendar.get(Calendar.DAY_OF_MONTH)
 
        return DatePickerDialog(
            requireContext(),
            null, // 实现 DatePickerDialog.OnDateSetListener 时设 null
            year, month, day
        )
    }
}

重写 onCreateDialog() 返回一个系统对话框——这是最简单的方式。

第二步:把选择的日期传回发起方

class DatePickerFragment : DialogFragment() {
 
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val calendar = Calendar.getInstance()
 
        return DatePickerDialog(
            requireContext(),
            { _, year, month, day ->
                val resultDate = GregorianCalendar(year, month, day).time
                val resultBundle = Bundle().apply {
                    putSerializable(KEY_DATE, resultDate)
                }
                setFragmentResult(REQUEST_KEY, resultBundle)
            },
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH),
            calendar.get(Calendar.DAY_OF_MONTH)
        )
    }
 
    companion object {
        const val REQUEST_KEY = "DATE_PICKER_RESULT"
        const val KEY_DATE = "selected_date"
    }
}

注意:DatePickerDialog 构造器的第二个参数就是回调——用户选完日期后触发。在这里把选好的日期打成 Bundle,用 setFragmentResult 发出去。

第三步:发起方监听结果

class CrimeDetailFragment : Fragment(R.layout.fragment_crime_detail) {
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
 
        childFragmentManager.setFragmentResultListener(
            DatePickerFragment.REQUEST_KEY,
            viewLifecycleOwner
        ) { _, bundle ->
            val date = bundle.getSerializable(DatePickerFragment.KEY_DATE) as Date
            updateDate(date)
        }
    }
 
    private fun showDatePicker() {
        DatePickerFragment().show(
            childFragmentManager,
            "DatePicker"
        )
    }
}

这里用的是 childFragmentManager 而不是 supportFragmentManager——因为对话框是当前 Fragment 的子 Fragment,用子 FragmentManager 管理。viewLifecycleOwner 确保只在 Fragment 的 View 活着的时候监听——View 销毁后自动取消监听,避免泄漏。

第四步:自定义布局的对话框

class InputDialogFragment : DialogFragment() {
 
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val view = LayoutInflater.from(requireContext())
            .inflate(R.layout.dialog_input, null, false)
 
        val editText = view.findViewById<EditText>(R.id.input_text)
 
        return AlertDialog.Builder(requireContext())
            .setView(view)
            .setTitle("输入嫌疑人姓名")
            .setPositiveButton("确定") { _, _ ->
                val result = editText.text.toString()
                setFragmentResult(
                    REQUEST_KEY,
                    Bundle().apply { putString(KEY_INPUT, result) }
                )
            }
            .setNegativeButton("取消", null)
            .create()
    }
}

.setView(view) 把自定义布局塞进对话框的"内容区域"。按钮处理中拿到的 editText 引用在回调触发时仍然有效(View 还没销毁)。

容易踩的坑

小结

  • AlertDialog 用 Builder 模式构建——setTitle/setMessage/setPositiveButton 链式调完后 build()
  • DialogFragment 是对话框的"安全壳"——旋转屏幕自动保存恢复,FragmentManager 统一管理
  • FragmentResult API 用 requestKey 配对,发起方 listener 必须在 show() 之前注册
  • 自定义布局用 setView(view),按钮回调中 View 引用仍然有效

练习

问题 1:“第13章 Dialogs”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

AlertDialog

Android 最通用的对话框类。通过 Builder 构建器模式(链式调用 setTitle/setMessage/setPositiveButton 等)构建,最后 show() 显示。它悬浮在 Activity 之上、遮挡背景——迫使用户做出选择。

DialogFragment

对话框的 Fragment 包装器。把对话框纳入 FragmentManager 管理——享受自动保存恢复、生命周期协同的好处。解决了普通 Dialog 旋转屏幕会消失的经典问题。

FragmentResult API

Fragment 间通信的现代方案。一对"请求 key" + Bundle 的配对机制:发送方 setFragmentResult,接收方 setFragmentResultListener。取代了已废弃的 setTargetFragment

“对话框”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“对话框”必须回到可观察状态

“对话框”的学习结果不是记住类名,而是能预测“掌握 AlertDialog 构建模式、DialogFragment 的配置变更安全机制、以及 FragmentResult API 的双向数据传递。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

13. Dialogs

在“对话框”中,分析“13. Dialogs”要标出事务提交、参数写入、目标结果和返回栈变化,避免用活动中的临时字段传递可恢复状态。

Creating a DialogFragment

在“对话框”中,验证“Creating a DialogFragment”在旋转、后台恢复和快速连点下比较fragment列表、back stack与目标数据,只出现一次导航副作用。

Passing Data Between Two Fragments

在“对话框”中,“Passing Data Between Two Fragments”通过显式Intent启动目标并以extras/result交换最小数据;发送方、接收方和进程重建都必须验证缺失、类型错误和重复返回。

Challenge: More Dialogs

在“对话框”中,“Challenge: More Dialogs”的容器替换不是简单换画面;状态保存后提交、重复tag与嵌套管理器都会改变恢复结果。

“对话框”验收回顾

“对话框”只有在双视口、大字体、焦点顺序、状态语义和对比度截图能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:Fragment Navigation · 下一页:AppBar与菜单 →

章专属可重放状态实验

先预测“打开、旋转、确认、取消与重复提交”发生后,DialogFragment、调用 Fragment 与 FragmentResult应怎样改变初始日期、选择值、结果键和已消费状态;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

对话框:状态合同

让 DatePicker DialogFragment 通过稳定结果合同更新 Crime 日期

验证场景

第四版正式目录节点

dialogs · 正常任务

13. Dialogs固定 SDK、设备配置和初始状态,触发“打开、旋转、确认、取消与重复提交”

状态所有者DialogFragment、调用 Fragment 与 FragmentResult
受控状态初始日期、选择值、结果键和已消费状态
触发事件打开、旋转、确认、取消与重复提交

冻结入口:13. Dialogs

记录DialogFragment、调用 Fragment 与 FragmentResult的初始初始日期、选择值、结果键和已消费状态

观察:dialog tag、结果 bundle、消费计数和 Crime 日期快照中的“13. Dialogs”轨迹

预期:由DialogFragment、调用 Fragment 与 FragmentResult提交初始日期、选择值、结果键和已消费状态,并持续满足“对话框重建不丢初值,结果只消费一次且取消不改事实”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“打开、旋转、确认、取消与重复提交”。每一步只允许DialogFragment、调用 Fragment 与 FragmentResult按职责提交状态,并持续核对“对话框重建不丢初值,结果只消费一次且取消不改事实”。

Deterministic event replay

对话框:事件轨迹

选择一次状态转换1 / 5

不变量:对话框重建不丢初值,结果只消费一次且取消不改事实

交付证据:dialog tag、结果 bundle、消费计数和 Crime 日期快照

实验三:章专属反例与同输入恢复

注入“旋转后旧监听器和新监听器都收到日期结果,数据库写入两次”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有dialog tag、结果 bundle、消费计数和 Crime 日期快照一起恢复才算修复。

Fault · cancel · restore

对话框:反例与恢复

故障:旋转后旧监听器和新监听器都收到日期结果,数据库写入两次

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“旋转后旧监听器和新监听器都收到日期结果,数据库写入两次”

3. 检查所有者一致

对话框重建不丢初值,结果只消费一次且取消不改事实

4. 核对结果一致

dialog tag、结果 bundle、消费计数和 Crime 日期快照

讨论

评论区加载中…