Skip to content

事件、路由与对话框 ​

区分交互的层级 ​

需求常用方式适用场景
点击、长按、拖动按钮回调、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('保存'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

push<T> 返回 Future<T?>,目标页通过 pop(result) 返回结果;系统返回或取消可能得到 null。示例中的「保存」只返回文字并展示消息,接入真正的数据写入时,需等待写入成功再关闭页面。

小型应用可先用 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;
}

点击遮罩或通过返回关闭时,也可能得到 null。在调用方 await confirmDelete(context) 后,如需更新页面或再次使用 context,先检查挂载状态。确认行为和真正的删除操作分开,便于处理失败与重试。

练习与验收 ​

空白标题不能保存;有效标题返回后显示成功消息;直接返回不显示保存消息。再加入删除确认,检查取消、点击遮罩和系统返回都不会误触发删除。

相关资料:表单校验、返回页面数据。

持续记录,持续理解。