弹窗是每个 App 都离不开的基础交互组件:确认操作、提示状态、选择选项、展示信息,都依赖各种弹窗。此前用 ArkTS 开发鸿蒙应用时,@CustomDialog 和 promptAction.showDialog 用得比较多;切换到 Flutter 后,发现它的弹窗体系更丰富,API 选择也多,新手容易混淆。本文将 Flutter 中最常用的 6 种弹窗/提示组件在鸿蒙上完整跑了一遍,给出可运行的代码与选型思路,供从 ArkTS 转向 Flutter 的开发者参考。
先理清核心概念。Flutter 的弹窗分为两大类:模态弹窗(必须完成操作后才能返回)和非模态提示(不阻断用户操作)。showDialog 出来的属于模态弹窗,SnackBar 属于非模态提示,showMenu 则是上下文菜单,介于两者之间。所有弹窗最终都走 Navigator 的 overlay,因此调用时需要 MaterialApp 或 WidgetsApp 的 context。这个前提对于 ArkTS 开发者来说并不陌生,鸿蒙弹窗同样依赖 UIContext。
Flutter 弹窗最核心的设计是 Future 模式。showDialog 返回一个 Future,弹窗关闭时通过 Navigator.pop(context, 返回值) 将结果带回去,调用方 await 这个 Future 拿到结果。相比之下,ArkTS 的弹窗使用 @CustomDialog 装饰器配合 open() 方法调用,没有这种 Future 模式。Future 模式的优势在于代码更线性,不用写回调或事件监听。初次从 ArkTS 转向 Flutter 时,习惯用全局变量存回调结果;改用 Flutter 后发现一个 await 就能搞定,开发体验提升明显。
一、AlertDialog:最常用的确认弹窗
最基本场景是“确定删除吗”这类二选一确认。Flutter 提供 AlertDialog 组件,通过 showDialog 函数弹出:
- Future<void> _showAlert() async {
- final r = await showDialog<String>(
- context: context,
- builder: (ctx) => AlertDialog(
- title: const Text('确认删除?'),
- content: const Text('删除后不可恢复,确定要继续吗?'),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(ctx, '取消'),
- child: const Text('取消'),
- ),
- TextButton(
- onPressed: () => Navigator.pop(ctx, '确定'),
- child: const Text('确定', style: TextStyle(color: Colors.red)),
- ),
- ],
- ),
- );
- if (r != null) setState(() => _result = r);
- }
复制代码
actions 里的按钮默认水平排列。若按钮较多,可包一个 Column 改为竖排。注意:不调 pop 或点击背景关闭时,await 的结果是 null,代码需判空。按钮样式上,默认 TextButton 是纯文字按钮不带背景;如需带背景,换 ElevatedButton。“取消”和“确定”可用 TextButton.styleFrom(foregroundColor: Colors.red) 将确认按钮标红,视觉区分更明显。
这里有一个关键易错点:showDialog 的 builder 回调传入的 ctx 是弹窗自己的 context,关闭弹窗要用 Navigator.pop(ctx) 而不是 Navigator.pop(context),后者会把整个页面关掉。
二、SimpleDialog:列表选择弹窗
当场景不是确认/取消,而是从多个选项中选一个时,用 SimpleDialog 配合 SimpleDialogOption 实现:
- final r = await showDialog<String>(
- context: context,
- builder: (ctx) => SimpleDialog(
- title: const Text('选择主题色'),
- children: [
- SimpleDialogOption(
- onPressed: () => Navigator.pop(ctx, '红色'),
- child: const ListTile(
- leading: Icon(Icons.circle, color: Colors.red),
- title: Text('红色'),
- ),
- ),
- // ... 更多选项
- ],
- ),
- );
复制代码
每个 SimpleDialogOption 点击后直接 pop 带回值,与 AlertDialog 相同的 Future 模式。用 ListTile 包装选项,可同时展示图标和文字,比纯文字选项更直观;直接用 Text 也可以,但缺少图标和副标题的扩展能力。
在 ArkTS 中对应方式是 AlertDialog 配合 ListItemGroup 或手写循环。相比而言,SimpleDialog 更省事,不需要自己处理滚动和布局,选项多了自动滚动。不过选项超过 7-8 个时弹窗高度会撑满屏幕,此时建议改用 BottomSheet 或专门的选择页。
三、BottomSheet:底部面板
从底部滑出的面板,常用于分享、操作列表、附加筛选条件等。通过 showModalBottomSheet 弹出:
- void _showSheet(BuildContext context) {
- showModalBottomSheet(
- context: context,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
- ),
- builder: (ctx) => SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- // 拖拽指示条
- Container(
- width: 40, height: 4,
- decoration: BoxDecoration(
- color: Colors.grey.shade300,
- borderRadius: BorderRadius.circular(2),
- ),
- ),
- const SizedBox(height: 16),
- const Text('分享到'),
- const Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- _ShareItem(icon: Icons.wechat, label: '微信'),
- _ShareItem(icon: Icons.message, label: '短信'),
- _ShareItem(icon: Icons.email, label: '邮件'),
- _ShareItem(icon: Icons.link, label: '链接'),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
复制代码
SafeArea 包一层可防止刘海屏和底部虚拟键遮挡;shape 参数配圆角让面板更精致;mainAxisSize: MainAxisSize.min 使面板只占内容高度。顶部加一条横线作为拖拽指示条是常见交互细节,告诉用户可下滑关闭。ArkTS 的 bindSheet 自带此指示条,Flutter 需要自己用 Container 画,实现成本不高。
注意 BottomSheet 有两种:showModalBottomSheet 是模态的,点背景或下滑关闭,需要手动 Navigator.pop 返回值;showBottomSheet 是非模态的,内嵌在父组件中,Scaffold 的 bottomSheet 参数也能挂一个永久底部面板。日常使用以 showModalBottomSheet 为主。
四、SnackBar:轻量提示
类似 Android 的 Toast 但更灵活,支持操作按钮,通过 ScaffoldMessenger 管理:
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('操作成功')),
- );
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: const Text('网络请求失败'),
- backgroundColor: Colors.red,
- duration: const Duration(seconds: 3),
- action: SnackBarAction(
- label: '重试',
- textColor: Colors.white,
- onPressed: () { /* 重试逻辑 */ },
- ),
- ),
- );
复制代码
SnackBar 自动消失,默认 4 秒,duration 可调;带 action 时适合“撤销”“重试”“查看”等场景。常见的坑是:SnackBar 必须在 Scaffold 的上下文中调用,在没有 Scaffold 的页面或自定义弹窗里调用会报找不到 ScaffoldMessenger。解决方法是 Builder 包一层,或在有 Scaffold 的祖先节点 context 上调用。ArkTS 里没有直接对应的 SnackBar,鸿蒙原生通常用 promptAction.showToast 或自绘底部提示;Flutter 的 SnackBar 用法固定、样式统一,省去造轮子。
频繁弹 SnackBar 时要注意:连续调用多次 showSnackBar 会排队而非丢弃。若某个操作可能被快速触发(如多次点击保存),建议先调 hideCurrentSnackBar() 清掉当前队列再弹新的。
五、PopupMenuButton:弹出菜单
适合“更多操作”场景。PopupMenuButton 组件包在页面上即可,点击后菜单在按钮附近弹出,无需手动计算坐标:
- PopupMenuButton<String>(
- onSelected: (v) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('选中:$v')),
- );
- },
- itemBuilder: (ctx) => const [
- PopupMenuItem(value: '编辑', child: ListTile(
- leading: Icon(Icons.edit), title: Text('编辑'))),
- PopupMenuItem(value: '收藏', child: ListTile(
- leading: Icon(Icons.star), title: Text('收藏'))),
- PopupMenuItem(value: '删除', child: ListTile(
- leading: Icon(Icons.delete, color: Colors.red),
- title: Text('删除', style: TextStyle(color: Colors.red)))),
- ],
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [Text('更多操作'), Icon(Icons.arrow_drop_down)],
- ),
- )
复制代码
onSelected 回调获取选中值,itemBuilder 返回菜单项列表。PopupMenuItem 的值类型泛型可自定义,不一定是 String。ArkTS 中对应 @CustomDialog 或 bindMenu 属性;bindMenu 用法也简单,但 PopupMenuButton 可定制性更高,菜单项里能放任意 Widget。菜单项带图标时建议用 ListTile,leading 放图标、title 放文字,效果接近系统菜单。也可用 showMenu 函数在任意位置手动弹出菜单,但 PopupMenuButton 更省事。
六、自定义弹窗:突破模板限制
当 AlertDialog 的默认布局不够用,可以用 Dialog 组件自由搭建。Dialog 只提供卡片背景和圆角,内容完全自由。以下是一个“支付成功”弹窗:大图标 + 金额 + 订单号 + 完成按钮:
- showDialog(
- context: context,
- builder: (ctx) => Dialog(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- const Icon(Icons.check_circle, color: Colors.red, size: 64),
- const SizedBox(height: 12),
- const Text('支付成功',
- style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
- const SizedBox(height: 8),
- Text('¥ 168.00',
- style: TextStyle(fontSize: 28,
- color: Colors.red.shade700,
- fontWeight: FontWeight.w600)),
- Text('订单号:20240628123456',
- style: TextStyle(fontSize: 12,
- color: Colors.grey.shade600)),
- const SizedBox(height: 20),
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () => Navigator.pop(ctx),
- child: const Text('完成'),
- ),
- ),
- ],
- ),
- ),
- ),
- );
复制代码
Column + mainAxisSize: Min 让内容紧贴,不高不矮。自定义弹窗注意:Dialog 默认有最大宽度限制,超过会加内边距;需要更宽弹窗时,用 insetPadding 参数调整,或直接用 AlertDialog 的 content 塞自定义布局。ArkTS 自定义弹窗用 @CustomDialog 装饰器,Flutter 则用 Dialog 直接嵌套布局 Widget,灵活性更高。
Flutter 与 ArkTS 弹窗对照
从 ArkTS 转 Flutter 的开发者可参考以下映射关系:AlertDialog 对应 ArkTS 的 @CustomDialog + 确认/取消按钮组合;SimpleDialog 对应 AlertDialog 配合 ListItemGroup;BottomSheet 对应 bindSheet 的 sheetBuilder;SnackBar 在 ArkTS 中无直接对应,用 promptAction.showToast 或自绘;PopupMenuButton 对应 bindMenu;自定义 Dialog 对应 @CustomDialog 自定义布局。
核心差异在编程模型:Flutter 弹窗走 showDialog + Navigator.pop 的 Future 模式,ArkTS 走 @CustomDialog 装饰器 + open() 方法。Flutter 的链式调用和 await 写法更接近直觉,ArkTS 的装饰器模式更符合声明式习惯。
踩坑记录
1. Navigator.pop 用错 context。showDialog 的 builder 中必须用回调传入的 ctx 执行 pop,用外层 context 会导致弹窗关不掉甚至把主页 pop 掉。
2. SnackBar 不显示。多因 ScaffoldMessenger.of(context) 拿到的 context 不在 Scaffold 子树内,比如在自定义弹窗里调用。解决:用 Builder 包一层,或传有 Scaffold 的 context。
3. BottomSheet 圆角不生效。showModalBottomSheet 的 shape 参数要传 RoundedRectangleBorder 并设置 borderRadius;不设置时默认直角,与 App 圆角风格违和。
4. SnackBar 排队。连续弹多条会排队,建议弹之前先调 hideCurrentSnackBar() 清掉当前队列。
5. AlertDialog 背景可点关闭但不放回值。barrierDismissible 默认为 true,点击背景关闭后 await 返回 null;需要禁止时设 barrierDismissible: false,一般判空即可。
总体跑下来,Flutter 的弹窗体系丰富程度高于 ArkTS,同样是模态弹窗,Flutter 分为 AlertDialog、SimpleDialog、Dialog 三层,从常用到完全自定义逐层递进,选型清晰。ArkTS 的弹窗则更统一,@CustomDialog 通吃所有场景,灵活但有学习门槛。Flutter 的 Future 模式是最大优势,await showDialog 拿到用户操作结果,代码线性无回调嵌套,这个设计 ArkTS 没有直接对应,两套体系各有优劣,按项目场景选择即可。 |