外观
事件、路由与对话框
区分交互的层级
| 需求 | 常用方式 | 适用场景 |
|---|---|---|
| 点击、长按、拖动 | 按钮回调、InkWell、GestureDetector | 表达用户操作意图 |
| 原始指针事件 | Listener | 确实需要追踪按下、移动和抬起 |
| 子组件通知父组件 | 构造参数中的回调 | 保存、删除、选中等业务事件 |
| 向祖先传递通知 | Notification、NotificationListener | 滚动通知等树内协作 |
| 展示短消息 | ScaffoldMessenger + SnackBar | 告知保存结果、提供撤销操作 |
嵌套的手势识别器可能竞争同一次交互。父级点击没有触发时,应检查命中测试和手势竞技场,不要直接套用浏览器 DOM 冒泡的理解。对于普通操作,优先使用带焦点与语义支持的按钮。
实例:编辑后返回结果
下面是完整的单文件示例,可以替换练习项目的 lib/main.dart。它包含输入校验、保存返回和取消返回三个行为。
dart
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: NotesHome()));
class NotesHome extends StatelessWidget {
const NotesHome({super.key});
Future<void> _openEditor(BuildContext context) async {
final title = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(builder: (_) => const EditNotePage()),
);
if (!context.mounted || title == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已保存:$title')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('个人笔记')),
body: Center(
child: FilledButton(
onPressed: () => _openEditor(context),
child: const Text('新建笔记'),
),
),
);
}
}
class EditNotePage extends StatefulWidget {
const EditNotePage({super.key});
@override
State<EditNotePage> createState() => _EditNotePageState();
}
class _EditNotePageState extends State<EditNotePage> {
final _formKey = GlobalKey<FormState>();
final _title = TextEditingController();
@override
void dispose() {
_title.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('编辑笔记')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _title,
decoration: const InputDecoration(labelText: '标题'),
validator: (value) => (value ?? '').trim().isEmpty
? '请输入标题'
: null,
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
if (!_formKey.currentState!.validate()) return;
Navigator.of(context).pop(_title.text.trim());
},
child: const Text('保存'),
),
],
),
),
),
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
push<T> 返回 Future<T?>,目标页通过 pop(result) 返回结果;系统返回或取消可能得到 null。示例中的「保存」只返回文字并展示消息,接入真正的数据写入时,需等待写入成功再关闭页面。
Navigator 与声明式路由
小型应用可先用 Navigator.push/pop 理解页面栈。需要深链接、浏览器地址栏同步或复杂嵌套导航时,再选择基于 Router 的方案,例如 go_router。Flutter 官方对多数应用已不再推荐命名路由作为默认方案,参见 导航指南。
底部导航切换也需要明确状态策略:切换时重建子页面,还是保留各标签页状态?IndexedStack 能保留已挂载子树,但会同时占用这些子树的资源。不要仅为切换底部标签就不断往同一个页面栈中 push。
对话框是一次异步结果
下面的函数可添加到任意已导入 Material 的页面文件中,从按钮回调中调用:
dart
Future<bool> confirmDelete(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('删除笔记?'),
content: const Text('删除后,这条笔记将从列表移除。'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('删除'),
),
],
),
);
return confirmed ?? false;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
点击遮罩或通过返回关闭时,也可能得到 null。在调用方 await confirmDelete(context) 后,如需更新页面或再次使用 context,先检查挂载状态。确认行为和真正的删除操作分开,便于处理失败与重试。
练习与验收
空白标题不能保存;有效标题返回后显示成功消息;直接返回不显示保存消息。再加入删除确认,检查取消、点击遮罩和系统返回都不会误触发删除。