第12章 - 对话框与反馈
嗨,朋友!我是长安。
好的用户体验离不开及时的反馈。用户点击按钮后,要让他知道发生了什么;操作成功了,要给个提示;需要确认了,要弹个对话框。这一章,我们来学习 Flutter 中的对话框和反馈机制。
📢 SnackBar - 底部提示
SnackBar 是一个轻量级的提示组件,从屏幕底部滑出,适合显示简短的提示信息。
基本用法
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('保存成功!'),
),
);
带操作按钮
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('已删除 1 条记录'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
// 撤销删除
},
),
),
);
自定义样式
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 10),
Text('操作成功'),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating, // 浮动样式
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
duration: const Duration(seconds: 2),
),
);
封装 SnackBar 工具类
class SnackBarUtil {
static void show(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
static void showSuccess(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 10),
Text(message),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
static void showError(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 10),
Text(message),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}
// 使用
SnackBarUtil.showSuccess(context, '保存成功');
SnackBarUtil.showError(context, '网络连接失败');
💬 AlertDialog - 警告对话框
AlertDialog 用于显示重要提示或确认操作。
基本用法
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// 执行退出登录
},
child: const Text('确定'),
),
],
),
);
获取对话框返回值
Future<void> _showDeleteConfirm() async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: const Text('删除后无法恢复,确定要删除吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
),
);
if (result == true) {
// 执行删除操作
print('用户确认删除');
}
}
自定义对话框内容
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择头像'),
content: SizedBox(
width: double.maxFinite,
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 4,
children: List.generate(8, (index) {
return GestureDetector(
onTap: () {
Navigator.pop(context, index);
},
child: CircleAvatar(
backgroundImage: NetworkImage(
'https://i.pravatar.cc/100?img=$index',
),
),
);
}),
),
),
),
);
禁止点击外部关闭
showDialog(
context: context,
barrierDismissible: false, // 点击外部不关闭
builder: (context) => AlertDialog(
title: const Text('重要提示'),
content: const Text('请阅读并同意用户协议'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('我已阅读并同意'),
),
],
),
);
📋 SimpleDialog - 简单选择对话框
SimpleDialog 适合展示选项列表。
Future<void> _showLanguageDialog() async {
final result = await showDialog<String>(
context: context,
builder: (context) => SimpleDialog(
title: const Text('选择语言'),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, '中文'),
child: const Text('中文'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'English'),
child: const Text('English'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, '日本語'),
child: const Text('日本語'),
),
],
),
);
if (result != null) {
print('选择了: $result');
}
}
📝 输入对话框
带有输入框的对话框:
Future<void> _showInputDialog() async {
final controller = TextEditingController();
final result = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加备注'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
hintText: '请输入备注内容',
border: OutlineInputBorder(),
),
maxLines: 3,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('保存'),
),
],
),
);
if (result != null && result.isNotEmpty) {
print('备注内容: $result');
}
}
📅 日期时间选择器
日期选择器
Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
locale: const Locale('zh'), // 需要配置本地化
helpText: '选择日期',
cancelText: '取消',
confirmText: '确定',
);
if (picked != null) {
print('选择的日期: ${picked.year}-${picked.month}-${picked.day}');
}
}
时间选择器
Future<void> _selectTime() async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
helpText: '选择时间',
cancelText: '取消',
confirmText: '确定',
);
if (picked != null) {
print('选择的时间: ${picked.hour}:${picked.minute}');
}
}
日期范围选择器
Future<void> _selectDateRange() async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
initialDateRange: DateTimeRange(
start: DateTime.now(),
end: DateTime.now().add(const Duration(days: 7)),
),
);
if (picked != null) {
print('开始: ${picked.start}, 结束: ${picked.end}');
}
}
📄 BottomSheet - 底部弹出面板
模态底部面板
void _showBottomSheet() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
onTap: () {
Navigator.pop(context);
// 打开相机
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选择'),
onTap: () {
Navigator.pop(context);
// 打开相册
},
),
ListTile(
leading: const Icon(Icons.cancel),
title: const Text('取消'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
可滚动的底部面板
void _showScrollableBottomSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true, // 允许全屏
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.25,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollController) {
return Container(
padding: const EdgeInsets.all(16),
child: ListView.builder(
controller: scrollController,
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text('选项 $index'),
onTap: () => Navigator.pop(context, index),
);
},
),
);
},
),
);
}
圆角底部面板
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 拖动指示器
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// 内容
const Text('底部面板内容'),
],
),
),
);
⏳ Loading 加载状态
简单加载对话框
void _showLoading() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
}
// 关闭加载
Navigator.pop(context);
带文字的加载对话框
void _showLoadingWithText() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Row(
children: const [
CircularProgressIndicator(),
SizedBox(width: 20),
Text('加载中...'),
],
),
),
);
}
封装 Loading 工具类
class LoadingUtil {
static bool _isShowing = false;
static void show(BuildContext context, {String? message}) {
if (_isShowing) return;
_isShowing = true;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => WillPopScope(
onWillPop: () async => false, // 禁止返回键关闭
child: Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
if (message != null) ...[
const SizedBox(height: 16),
Text(message),
],
],
),
),
),
),
);
}
static void hide(BuildContext context) {
if (!_isShowing) return;
_isShowing = false;
Navigator.pop(context);
}
}
// 使用示例
Future<void> _submitForm() async {
LoadingUtil.show(context, message: '提交中...');
try {
await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求
LoadingUtil.hide(context);
SnackBarUtil.showSuccess(context, '提交成功');
} catch (e) {
LoadingUtil.hide(context);
SnackBarUtil.showError(context, '提交失败');
}
}
🔔 Toast 提示(使用第三方库)
fluttertoast 提供类似原生的 Toast 提示:
dependencies:
fluttertoast: ^8.2.4
import 'package:fluttertoast/fluttertoast.dart';
Fluttertoast.showToast(
msg: "这是一条 Toast 消息",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.black87,
textColor: Colors.white,
fontSize: 16.0,
);
🎯 实战:完整的反馈示例
class FeedbackDemoPage extends StatefulWidget {
const FeedbackDemoPage({super.key});
State<FeedbackDemoPage> createState() => _FeedbackDemoPageState();
}
class _FeedbackDemoPageState extends State<FeedbackDemoPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('反馈示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
ElevatedButton(
onPressed: () => _showSnackBar(),
child: const Text('显示 SnackBar'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => _showAlertDialog(),
child: const Text('显示确认对话框'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => _showInputDialog(),
child: const Text('显示输入对话框'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => _showBottomSheet(),
child: const Text('显示底部面板'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => _showDatePicker(),
child: const Text('选择日期'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => _simulateSubmit(),
child: const Text('模拟提交(带 Loading)'),
),
],
),
);
}
void _showSnackBar() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('这是一条提示消息'),
action: SnackBarAction(
label: '撤销',
onPressed: () {},
),
behavior: SnackBarBehavior.floating,
),
);
}
Future<void> _showAlertDialog() async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: const Text('确定要删除这条数据吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
),
);
if (result == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已删除')),
);
}
}
Future<void> _showInputDialog() async {
final controller = TextEditingController();
final result = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('输入备注'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
hintText: '请输入内容',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('确定'),
),
],
),
);
if (result != null && result.isNotEmpty && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('输入内容: $result')),
);
}
}
void _showBottomSheet() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
ListTile(
leading: const Icon(Icons.share),
title: const Text('分享'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.link),
title: const Text('复制链接'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.report),
title: const Text('举报'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
Future<void> _showDatePicker() async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (date != null && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('选择了: ${date.year}-${date.month}-${date.day}')),
);
}
}
Future<void> _simulateSubmit() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('提交中...'),
],
),
),
),
),
);
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
Icon(Icons.check, color: Colors.white),
SizedBox(width: 10),
Text('提交成功!'),
],
),
backgroundColor: Colors.green,
),
);
}
}
}
📝 小结
这一章我们学习了:
- ✅ SnackBar - 底部提示消息
- ✅ AlertDialog - 警告/确认对话框
- ✅ SimpleDialog - 简单选择对话框
- ✅ 日期时间选择器
- ✅ BottomSheet - 底部弹出面板
- ✅ Loading 加载状态
- ✅ 封装工具类提高开发效率
用户反馈最佳实践
- 操作后给反馈 - 让用户知道操作结果
- 重要操作需确认 - 删除等不可逆操作要二次确认
- 加载状态要显示 - 网络请求时显示加载中
- 错误信息要友好 - 不要显示技术性错误信息
💪 练习题
- 创建一个删除确认对话框,带「再也不提示」选项
- 实现一个图片选择底部面板(拍照/相册/取消)
- 封装一个通用的 Loading 工具类
🚀 下一步
掌握了对话框与反馈后,下一章我们来学习 列表进阶,实现下拉刷新和无限滚动!
由 编程指南 提供
