查看: 166|回复: 3

Flutter 鸿蒙弹窗实战:6 种弹窗组件从入门到优雅

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
弹窗是每个 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 函数弹出:
  1. Future<void> _showAlert() async {
  2.   final r = await showDialog<String>(
  3.     context: context,
  4.     builder: (ctx) => AlertDialog(
  5.       title: const Text('确认删除?'),
  6.       content: const Text('删除后不可恢复,确定要继续吗?'),
  7.       actions: [
  8.         TextButton(
  9.           onPressed: () => Navigator.pop(ctx, '取消'),
  10.           child: const Text('取消'),
  11.         ),
  12.         TextButton(
  13.           onPressed: () => Navigator.pop(ctx, '确定'),
  14.           child: const Text('确定', style: TextStyle(color: Colors.red)),
  15.         ),
  16.       ],
  17.     ),
  18.   );
  19.   if (r != null) setState(() => _result = r);
  20. }
复制代码

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 实现:
  1. final r = await showDialog<String>(
  2.   context: context,
  3.   builder: (ctx) => SimpleDialog(
  4.     title: const Text('选择主题色'),
  5.     children: [
  6.       SimpleDialogOption(
  7.         onPressed: () => Navigator.pop(ctx, '红色'),
  8.         child: const ListTile(
  9.           leading: Icon(Icons.circle, color: Colors.red),
  10.           title: Text('红色'),
  11.         ),
  12.       ),
  13.       // ... 更多选项
  14.     ],
  15.   ),
  16. );
复制代码

每个 SimpleDialogOption 点击后直接 pop 带回值,与 AlertDialog 相同的 Future 模式。用 ListTile 包装选项,可同时展示图标和文字,比纯文字选项更直观;直接用 Text 也可以,但缺少图标和副标题的扩展能力。

在 ArkTS 中对应方式是 AlertDialog 配合 ListItemGroup 或手写循环。相比而言,SimpleDialog 更省事,不需要自己处理滚动和布局,选项多了自动滚动。不过选项超过 7-8 个时弹窗高度会撑满屏幕,此时建议改用 BottomSheet 或专门的选择页。

三、BottomSheet:底部面板

从底部滑出的面板,常用于分享、操作列表、附加筛选条件等。通过 showModalBottomSheet 弹出:
  1. void _showSheet(BuildContext context) {
  2.   showModalBottomSheet(
  3.     context: context,
  4.     shape: const RoundedRectangleBorder(
  5.       borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
  6.     ),
  7.     builder: (ctx) => SafeArea(
  8.       child: Padding(
  9.         padding: const EdgeInsets.all(16),
  10.         child: Column(
  11.           mainAxisSize: MainAxisSize.min,
  12.           children: [
  13.             // 拖拽指示条
  14.             Container(
  15.               width: 40, height: 4,
  16.               decoration: BoxDecoration(
  17.                 color: Colors.grey.shade300,
  18.                 borderRadius: BorderRadius.circular(2),
  19.               ),
  20.             ),
  21.             const SizedBox(height: 16),
  22.             const Text('分享到'),
  23.             const Row(
  24.               mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  25.               children: [
  26.                 _ShareItem(icon: Icons.wechat, label: '微信'),
  27.                 _ShareItem(icon: Icons.message, label: '短信'),
  28.                 _ShareItem(icon: Icons.email, label: '邮件'),
  29.                 _ShareItem(icon: Icons.link, label: '链接'),
  30.               ],
  31.             ),
  32.           ],
  33.         ),
  34.       ),
  35.     ),
  36.   );
  37. }
复制代码

SafeArea 包一层可防止刘海屏和底部虚拟键遮挡;shape 参数配圆角让面板更精致;mainAxisSize: MainAxisSize.min 使面板只占内容高度。顶部加一条横线作为拖拽指示条是常见交互细节,告诉用户可下滑关闭。ArkTS 的 bindSheet 自带此指示条,Flutter 需要自己用 Container 画,实现成本不高。

注意 BottomSheet 有两种:showModalBottomSheet 是模态的,点背景或下滑关闭,需要手动 Navigator.pop 返回值;showBottomSheet 是非模态的,内嵌在父组件中,Scaffold 的 bottomSheet 参数也能挂一个永久底部面板。日常使用以 showModalBottomSheet 为主。

四、SnackBar:轻量提示

类似 Android 的 Toast 但更灵活,支持操作按钮,通过 ScaffoldMessenger 管理:
  1. ScaffoldMessenger.of(context).showSnackBar(
  2.   const SnackBar(content: Text('操作成功')),
  3. );
  4. ScaffoldMessenger.of(context).showSnackBar(
  5.   SnackBar(
  6.     content: const Text('网络请求失败'),
  7.     backgroundColor: Colors.red,
  8.     duration: const Duration(seconds: 3),
  9.     action: SnackBarAction(
  10.       label: '重试',
  11.       textColor: Colors.white,
  12.       onPressed: () { /* 重试逻辑 */ },
  13.     ),
  14.   ),
  15. );
复制代码

SnackBar 自动消失,默认 4 秒,duration 可调;带 action 时适合“撤销”“重试”“查看”等场景。常见的坑是:SnackBar 必须在 Scaffold 的上下文中调用,在没有 Scaffold 的页面或自定义弹窗里调用会报找不到 ScaffoldMessenger。解决方法是 Builder 包一层,或在有 Scaffold 的祖先节点 context 上调用。ArkTS 里没有直接对应的 SnackBar,鸿蒙原生通常用 promptAction.showToast 或自绘底部提示;Flutter 的 SnackBar 用法固定、样式统一,省去造轮子。

频繁弹 SnackBar 时要注意:连续调用多次 showSnackBar 会排队而非丢弃。若某个操作可能被快速触发(如多次点击保存),建议先调 hideCurrentSnackBar() 清掉当前队列再弹新的。

五、PopupMenuButton:弹出菜单

适合“更多操作”场景。PopupMenuButton 组件包在页面上即可,点击后菜单在按钮附近弹出,无需手动计算坐标:
  1. PopupMenuButton<String>(
  2.   onSelected: (v) {
  3.     ScaffoldMessenger.of(context).showSnackBar(
  4.       SnackBar(content: Text('选中:$v')),
  5.     );
  6.   },
  7.   itemBuilder: (ctx) => const [
  8.     PopupMenuItem(value: '编辑', child: ListTile(
  9.       leading: Icon(Icons.edit), title: Text('编辑'))),
  10.     PopupMenuItem(value: '收藏', child: ListTile(
  11.       leading: Icon(Icons.star), title: Text('收藏'))),
  12.     PopupMenuItem(value: '删除', child: ListTile(
  13.       leading: Icon(Icons.delete, color: Colors.red),
  14.       title: Text('删除', style: TextStyle(color: Colors.red)))),
  15.   ],
  16.   child: Row(
  17.     mainAxisSize: MainAxisSize.min,
  18.     children: [Text('更多操作'), Icon(Icons.arrow_drop_down)],
  19.   ),
  20. )
复制代码

onSelected 回调获取选中值,itemBuilder 返回菜单项列表。PopupMenuItem 的值类型泛型可自定义,不一定是 String。ArkTS 中对应 @CustomDialog 或 bindMenu 属性;bindMenu 用法也简单,但 PopupMenuButton 可定制性更高,菜单项里能放任意 Widget。菜单项带图标时建议用 ListTile,leading 放图标、title 放文字,效果接近系统菜单。也可用 showMenu 函数在任意位置手动弹出菜单,但 PopupMenuButton 更省事。

六、自定义弹窗:突破模板限制

当 AlertDialog 的默认布局不够用,可以用 Dialog 组件自由搭建。Dialog 只提供卡片背景和圆角,内容完全自由。以下是一个“支付成功”弹窗:大图标 + 金额 + 订单号 + 完成按钮:
  1. showDialog(
  2.   context: context,
  3.   builder: (ctx) => Dialog(
  4.     shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
  5.     child: Padding(
  6.       padding: const EdgeInsets.all(24),
  7.       child: Column(
  8.         mainAxisSize: MainAxisSize.min,
  9.         children: [
  10.           const Icon(Icons.check_circle, color: Colors.red, size: 64),
  11.           const SizedBox(height: 12),
  12.           const Text('支付成功',
  13.             style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
  14.           const SizedBox(height: 8),
  15.           Text('¥ 168.00',
  16.             style: TextStyle(fontSize: 28,
  17.               color: Colors.red.shade700,
  18.               fontWeight: FontWeight.w600)),
  19.           Text('订单号:20240628123456',
  20.             style: TextStyle(fontSize: 12,
  21.               color: Colors.grey.shade600)),
  22.           const SizedBox(height: 20),
  23.           SizedBox(
  24.             width: double.infinity,
  25.             child: ElevatedButton(
  26.               onPressed: () => Navigator.pop(ctx),
  27.               child: const Text('完成'),
  28.             ),
  29.           ),
  30.         ],
  31.       ),
  32.     ),
  33.   ),
  34. );
复制代码

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 没有直接对应,两套体系各有优劣,按项目场景选择即可。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: Flutter 鸿蒙弹窗实战:6 种弹窗组件从入门到优雅

楼主这篇写得很实用,尤其是从 ArkTS 转到 Flutter 的对比视角,对正在过渡的开发者帮助很大。Future 模式那段确实是 Flutter 弹窗最香的地方,await 一行拿结果比回调爽太多了。另外 builder 里那个 ctx 的易错点提醒得很到位,新手确实容易搞混。期待看到 BottomSheet 之后的部分,比如 showMenu 和 SnackBar 的实战。
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: Flutter 鸿蒙弹窗实战:6 种弹窗组件从入门到优雅

写得很实用,特别是那个 `Navigator.pop(ctx)` 和 `Navigator.pop(context)` 的坑,确实容易踩。从 ArkTS 转过来之后,最不习惯的就是 Future 模式,不过用熟了确实比回调舒服。 另外想问一下,标题里说 6 种弹窗组件,目前只看到了 AlertDialog、SimpleDialog、BottomSheet 这三种,后面还有哪三种?比如 Dialog 自定义弹窗、showMenu、SnackBar 这些会在后面讲到吗?期待更新。
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: Flutter 鸿蒙弹窗实战:6 种弹窗组件从入门到优雅

写得很实用,尤其是 Future 模式和 Navigator.pop 那个易错点,刚转过来的人确实容易踩坑。简单列一下我对这 6 种弹窗的感受: 1. AlertDialog 确实是日常里用得最多的,判空那个提醒很关键; 2. SimpleDialog 做多选一挺顺手,选项多的时候换 BottomSheet 或者页面是合理建议; 3. BottomSheet 这节内容好像被截断了?如果方便可以把后面的也补全,大家一起学习下。 另外想问下楼主,SnackBar 在鸿蒙上有没有遇到什么坑?比如悬浮窗权限或者显示位置需要特别处理吗?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-11 20:52 , Processed in 0.023708 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部