对话框
对话框:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“对话框”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 AlertDialog 构建模式、DialogFragment 的配置变更安全机制、以及 FragmentResult API 的双向数据传递。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用双视口、大字体、焦点顺序、状态语义和对比度截图独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么需要对话框?
你在用 App 时肯定见过这些场景:弹出一个窗口问"确定删除吗?"、选个日期、输个名字。这些都是对话框——一种悬浮在主界面之上的轻量级 UI。
对话框的核心价值:打断用户当前操作,强制做出一个选择后再继续。它不是"顺便问问你"的那种弱提醒(那是 Snackbar/Toast 的活),而是"你不回答我就不让你往下走"的强交互。
用得好,对话框让交互清晰且安全(删除前确认)。用不好,对话框满天飞就是用户体验灾难——所以 Android 提供了一套规范的对话框 API,把常见的模式(确认、输入、选择、日期)都帮你封装好了。
对话框是怎么工作的
AlertDialog:最通用的对话框
↡AlertDialog 是 Android 最通用的对话框类。用 Builder 构建器模式(链式调用 setTitle、setMessage、setPositiveButton 等),最后 build() 创建、show() 显示。使用构建器模式——你先用 AlertDialog.Builder
一点点搭好对话框的样子(标题、内容、按钮),最后 build()
创建它。三个区域的按钮分工明确:
- Positive 按钮(确定/保存)——肯定操作,通常在右侧
- Negative 按钮(取消)——否定操作,Positive 左边
- Neutral 按钮(稍后提醒)——中立操作,最左边
DialogFragment:配置变更的安全网
为什么不能直接在 Activity 里 AlertDialog.Builder(...).show()?
问题出在屏幕旋转。当你旋转手机时,Activity 被销毁重建——但那个正在显示的对话框不属于 Activity 生命周期管理范围,重建后对话框消失了,用户之前的选择也丢了。DialogFragment 解决了这个问题:它本身是一个 Fragment,受 FragmentManager 管理,旋转屏幕时跟普通 Fragment 一样被保存和恢复。
- ↡DialogFragment 是一个特殊的 Fragment——它是对话框的「安全壳」。对话框的生命周期跟 DialogFragment 绑定,享受 FragmentManager 的自动保存和恢复。 就是一个"包着对话框的 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()))① 创建 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、设备配置和初始状态,触发“打开、旋转、确认、取消与重复提交”
冻结入口:13. Dialogs
记录DialogFragment、调用 Fragment 与 FragmentResult的初始初始日期、选择值、结果键和已消费状态
观察:dialog tag、结果 bundle、消费计数和 Crime 日期快照中的“13. Dialogs”轨迹
预期:由DialogFragment、调用 Fragment 与 FragmentResult提交初始日期、选择值、结果键和已消费状态,并持续满足“对话框重建不丢初值,结果只消费一次且取消不改事实”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“打开、旋转、确认、取消与重复提交”。每一步只允许DialogFragment、调用 Fragment 与 FragmentResult按职责提交状态,并持续核对“对话框重建不丢初值,结果只消费一次且取消不改事实”。
Deterministic event replay
对话框:事件轨迹
不变量:对话框重建不丢初值,结果只消费一次且取消不改事实
交付证据:dialog tag、结果 bundle、消费计数和 Crime 日期快照
实验三:章专属反例与同输入恢复
注入“旋转后旧监听器和新监听器都收到日期结果,数据库写入两次”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有dialog tag、结果 bundle、消费计数和 Crime 日期快照一起恢复才算修复。
Fault · cancel · restore
对话框:反例与恢复
故障:旋转后旧监听器和新监听器都收到日期结果,数据库写入两次
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“旋转后旧监听器和新监听器都收到日期结果,数据库写入两次”
对话框重建不丢初值,结果只消费一次且取消不改事实
dialog tag、结果 bundle、消费计数和 Crime 日期快照