前言:
本讲聚焦Flutter中用户交互反馈类组件,这是很重要的一个交互窗口,完成这一讲,再看看15/16讲(存储相关),已经可以在不与后端交互的过程中,通过AI开发所有东西了。
一、本讲内容
- 悬浮按钮(FloatingActionButton):提供页面核心功能的快捷入口
- 对话框(AlertDialog/SimpleDialog):阻断式信息展示与操作确认
- 底部弹窗(BottomSheet):非阻断式的底部操作面板
- 提示组件(SnackBar/Toast):轻量级操作结果反馈
这些组件是构建「友好用户体验」的核心,几乎所有商用App(如电商、社交、工具类)都会高频使用。
结合状态管理(Provider/Bloc)实现弹窗状态控制、适配多端(移动端/桌面端)交互规范
-
组件层级:
- 页面级组件(FAB/SnackBar/BottomSheet)依赖
Scaffold - 应用级组件(Dialog/Toast)挂载到
Overlay层
- 页面级组件(FAB/SnackBar/BottomSheet)依赖
-
交互类型:
- 阻断式(AlertDialog/SimpleDialog/ModalBottomSheet):需主动关闭
- 非阻断式(FAB/SnackBar/Toast):自动消失或不影响操作
-
使用场景:
- FAB:页面核心功能快捷入口
- Dialog:二次确认/信息展示
- BottomSheet:多选项操作面板
- SnackBar/Toast:轻量级操作结果反馈
再次记忆:
-
组件层级:
- 页面级组件(FAB/SnackBar/BottomSheet)依赖
Scaffold - 应用级组件(Dialog/Toast)挂载到
Overlay层
- 页面级组件(FAB/SnackBar/BottomSheet)依赖
-
交互类型:
- 阻断式(AlertDialog/SimpleDialog/ModalBottomSheet):需主动关闭
- 非阻断式(FAB/SnackBar/Toast):自动消失或不影响操作
-
使用场景:
- FAB:页面核心功能快捷入口
- Dialog:二次确认/信息展示
- BottomSheet:多选项操作面板
- SnackBar/Toast:轻量级操作结果反馈
-
挂载层级:
- 悬浮按钮/BottomSheet/SnackBar 依赖
Scaffold容器,属于页面级组件 - 对话框/Toast 挂载到
Overlay层(独立于页面的全局层),属于应用级组件
- 悬浮按钮/BottomSheet/SnackBar 依赖
-
交互特性:
- 阻断式组件(AlertDialog/SimpleDialog):阻塞页面其他操作,需用户主动关闭
- 非阻断式组件(SnackBar/BottomSheet/Toast/FAB):不影响页面其他操作
二、组件内容
2.1 FloatingActionButton(悬浮按钮)
核心属性
| 属性名 | 作用 | 常用值 |
|---|---|---|
onPressed | 点击回调 | () => {} |
child | 按钮内容 | Icon/Text |
backgroundColor | 背景色 | Colors.xxx |
foregroundColor | 前景色 | Colors.xxx |
elevation | 阴影高度 | 0-24 |
shape | 按钮形状 | CircleBorder()/RoundedRectangleBorder() |
mini | 是否迷你尺寸 | true/false |
核心案例
import 'package:flutter/material.dart';
class FABExample extends StatelessWidget {
const FABExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("悬浮按钮示例")),
// 核心悬浮按钮
floatingActionButton: FloatingActionButton(
onPressed: () {
// 点击事件
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("悬浮按钮被点击")),
);
},
backgroundColor: Colors.blue, // 背景色
foregroundColor: Colors.white, // 前景色(图标/文字)
elevation: 8, // 阴影高度
shape: RoundedRectangleBorder( // 自定义形状(默认圆形)
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.add), // 按钮内容
),
// 按钮位置(默认右下角)
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
注意事项
- 必须挂载在
Scaffold的floatingActionButton属性上 - 避免设置过大尺寸,遵循Material Design规范
- 一个页面建议只放一个核心FAB,多个可使用
FloatingActionButton.extended或BottomAppBar配合
2.2 AlertDialog/SimpleDialog(对话框)
核心属性
| 组件 | 核心属性 | 作用 |
|---|---|---|
| AlertDialog | title | 对话框标题 |
content | 对话框内容 | |
actions | 底部操作按钮列表 | |
backgroundColor | 对话框背景色 | |
| SimpleDialog | title | 对话框标题 |
children | 选项列表(SimpleDialogOption) | |
titlePadding | 标题内边距 |
核心案例
class DialogExample extends StatelessWidget {
const DialogExample({super.key});
// 显示AlertDialog
void _showAlertDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false, // 点击空白处是否关闭(false=不关闭)
builder: (context) {
return AlertDialog(
title: const Text("确认删除?"), // 标题
content: const Text("删除后数据将无法恢复,是否确认?"), // 内容
// 操作按钮
actions: [
TextButton(
onPressed: () => Navigator.pop(context), // 关闭对话框
child: const Text("取消"),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("数据已删除")),
);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text("确认"),
),
],
);
},
);
}
// 显示SimpleDialog(选择型对话框)
void _showSimpleDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text("选择语言"),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, "中文"),
child: const Text("中文"),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, "英文"),
child: const Text("英文"),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, "日语"),
child: const Text("日语"),
),
],
);
},
).then((value) {
if (value != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("你选择了:$value")),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("对话框示例")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _showAlertDialog(context),
child: const Text("显示确认对话框"),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _showSimpleDialog(context),
child: const Text("显示选择对话框"),
),
],
),
),
);
}
}
注意事项
- 通过
showDialog方法触发,需传入context - 关闭对话框必须调用
Navigator.pop(context) barrierDismissible设为false时,必须提供关闭按钮- SimpleDialog适合「单选场景」,AlertDialog适合「确认场景」
2.3 BottomSheet(底部弹窗)
核心属性
| 属性名 | 作用 | 常用值 |
|---|---|---|
isScrollControlled | 是否可滚动(适配高度) | true/false |
shape | 弹窗形状 | RoundedRectangleBorder() |
backgroundColor | 背景色 | Colors.xxx |
elevation | 阴影高度 | 0-24 |
builder | 弹窗内容构建器 | (context) => Widget |
核心案例
class BottomSheetExample extends StatelessWidget {
const BottomSheetExample({super.key});
void _showBottomSheet(BuildContext context) {
// 模态底部弹窗(阻断式)
showModalBottomSheet(
context: context,
isScrollControlled: true, // 是否可滚动(适配软键盘)
shape: const RoundedRectangleBorder( // 顶部圆角
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return SizedBox(
height: 220,
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text("底部操作面板", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
ListTile(
leading: const Icon(Icons.share),
title: const Text("分享"),
onTap: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("分享成功")),
);
},
),
ListTile(
leading: const Icon(Icons.delete),
title: const Text("删除"),
textColor: Colors.red,
onTap: () {
Navigator.pop(context);
_showDeleteConfirm(context);
},
),
ListTile(
leading: const Icon(Icons.cancel),
title: const Text("取消"),
onTap: () => Navigator.pop(context),
),
],
),
);
},
);
}
void _showDeleteConfirm(BuildContext context) {
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);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("删除成功")));
}, child: const Text("确认")),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("底部弹窗示例")),
body: Center(
child: ElevatedButton(
onPressed: () => _showBottomSheet(context),
child: const Text("显示底部弹窗"),
),
),
);
}
}
注意事项
showModalBottomSheet是阻断式弹窗,需主动关闭- 内容超过屏幕高度时,需嵌套
ListView或SingleChildScrollView - 非模态BottomSheet可通过
Scaffold的bottomSheet属性实现
2.4 SnackBar/Toast(提示反馈)
核心属性
| 组件 | 核心属性 | 作用 |
|---|---|---|
| SnackBar | content | 提示文本 |
backgroundColor | 背景色 | |
duration | 显示时长 | |
action | 右侧操作按钮 | |
behavior | 显示位置 | |
| Toast(自定义) | message | 提示文本 |
duration | 显示时长 | |
position | 显示位置 |
核心案例
// 先实现Toast(Flutter无原生Toast,自定义实现)
class Toast {
static void show(BuildContext context, String message) {
final overlay = Overlay.of(context);
final overlayEntry = OverlayEntry(
builder: (context) => Positioned(
bottom: 50,
left: MediaQuery.of(context).size.width * 0.1,
right: MediaQuery.of(context).size.width * 0.1,
child: Material(
borderRadius: BorderRadius.circular(8),
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
),
),
),
),
);
overlay.insert(overlayEntry);
// 2秒后移除
Future.delayed(const Duration(seconds: 2), () => overlayEntry.remove());
}
}
class FeedbackExample extends StatelessWidget {
const FeedbackExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("提示反馈示例")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// SnackBar示例
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar() // 隐藏之前的SnackBar
..showSnackBar(
SnackBar(
content: const Text("操作成功!"),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3), // 显示时长
action: SnackBarAction( // 可操作的SnackBar
label: "撤销",
textColor: Colors.white,
onPressed: () {
Toast.show(context, "已撤销操作");
},
),
),
);
},
child: const Text("显示SnackBar"),
),
const SizedBox(height: 20),
// Toast示例
ElevatedButton(
onPressed: () => Toast.show(context, "这是一个Toast提示"),
child: const Text("显示Toast"),
),
],
),
),
);
}
}
注意事项
- SnackBar必须通过
ScaffoldMessenger展示,依赖Scaffold - SnackBar默认显示在底部,可通过
behavior改为悬浮式 - Flutter无原生Toast,建议使用第三方库(如
fluttertoast)或自定义 - Toast是轻量级提示,不建议放复杂操作,显示时长控制在1-3秒
三、应用案例
实现一个「待办事项」页面,包含:
- 悬浮按钮:添加新待办
- 点击待办:弹出确认对话框
- 操作结果:SnackBar/Toast提示
- 更多操作:底部弹窗展示
import 'package:flutter/material.dart';
// 自定义Toast组件
class Toast {
static void show(BuildContext context, String message) {
final overlay = Overlay.of(context);
final overlayEntry = OverlayEntry(
builder: (context) => Positioned(
bottom: 50,
left: MediaQuery.of(context).size.width * 0.1,
right: MediaQuery.of(context).size.width * 0.1,
child: Material(
borderRadius: BorderRadius.circular(8),
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
),
),
);
overlay.insert(overlayEntry);
Future.delayed(const Duration(seconds: 2), () => overlayEntry.remove());
}
}
// 待办事项模型
class Todo {
final String title;
final bool isCompleted;
Todo({required this.title, this.isCompleted = false});
}
class TodoApp extends StatefulWidget {
const TodoApp({super.key});
@override
State<TodoApp> createState() => _TodoAppState();
}
class _TodoAppState extends State<TodoApp> {
final List<Todo> _todos = [ Todo(title: "学习Flutter弹窗组件"), Todo(title: "完成作业", isCompleted: true), Todo(title: "整理笔记"), ];
// 添加新待办
void _addNewTodo() {
showDialog(
context: context,
builder: (context) {
final TextEditingController controller = TextEditingController();
return AlertDialog(
title: const Text("添加新待办"),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: "请输入待办内容"),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("取消"),
),
TextButton(
onPressed: () {
if (controller.text.trim().isEmpty) {
Toast.show(context, "内容不能为空");
return;
}
setState(() {
_todos.add(Todo(title: controller.text.trim()));
});
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("已添加:${controller.text.trim()}"),
backgroundColor: Colors.green,
),
);
},
child: const Text("添加"),
),
],
);
},
);
}
// 显示更多操作底部弹窗
void _showMoreActions(BuildContext context, Todo todo) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SizedBox(
height: 180,
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text("操作选项", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
ListTile(
leading: Icon(todo.isCompleted ? Icons.check_circle : Icons.radio_button_unchecked),
title: Text(todo.isCompleted ? "标记为未完成" : "标记为完成"),
onTap: () {
Navigator.pop(context);
setState(() {
_todos[_todos.indexOf(todo)] = Todo(
title: todo.title,
isCompleted: !todo.isCompleted,
);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(todo.isCompleted ? "已标记为未完成" : "已标记为完成"),
),
);
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text("删除", style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(context);
_confirmDelete(context, todo);
},
),
],
),
),
);
}
// 确认删除对话框
void _confirmDelete(BuildContext context, Todo todo) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("确认删除?"),
content: Text("是否删除待办:${todo.title}"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("取消"),
),
TextButton(
onPressed: () {
setState(() {
_todos.remove(todo);
});
Navigator.pop(context);
Toast.show(context, "已删除:${todo.title}");
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text("删除"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("待办事项管理"),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () => Toast.show(context, "更多功能开发中"),
),
],
),
body: _todos.isEmpty
? const Center(child: Text("暂无待办事项,点击右下角添加"))
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return ListTile(
leading: Icon(
todo.isCompleted ? Icons.check_circle : Icons.radio_button_unchecked,
color: todo.isCompleted ? Colors.green : Colors.grey,
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
color: todo.isCompleted ? Colors.grey : Colors.black,
),
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => _showMoreActions(context, todo),
);
},
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
onPressed: _addNewTodo,
backgroundColor: Colors.blue,
child: const Icon(Icons.add),
),
);
}
}
// 入口函数
void main() {
runApp(const MaterialApp(
home: TodoApp(),
debugShowCheckedModeBanner: false,
));
}
- 首页:展示待办事项列表,右下角有添加悬浮按钮
- 添加待办:点击悬浮按钮弹出输入对话框,输入完成后SnackBar提示
- 操作待办:点击待办项弹出底部操作弹窗,可标记完成/未完成、删除
- 删除确认:点击删除后弹出确认对话框,确认后Toast提示删除成功
- 空内容提示:添加待办时内容为空,Toast提示错误信息