背景
Flutter 本身已经提供了多种弹窗能力,但在真实业务中,一个问题很快会暴露出来:
❗"能弹窗"并不等于"弹窗好用"。
在高频交互场景里(如编辑器确认、流程阻塞、上传 loading、分流决策等),弹窗不仅是 UI 表现,更是业务流程的一部分。此时,如果缺乏统一的设计约束,很容易出现:
- 同类弹窗使用方式不一致(有的返回 Future,有的没有)
- 关闭行为不统一(点击外部 / 强制决策混杂)
- 调用方式分散(overlay / dialog / 自定义实现并存)
这些问题本质上并不是"实现问题",而是语义没有被统一建模。
本文写了什么
本文将基于一个真实的 Dialog 模块演进过程,从设计、使用到重构,尝试回答一个更具工程价值的问题:
👉 一个公共弹窗组件库,应该如何设计,才能在复杂业务中长期可用?
文章将围绕两个核心展开:
- 在真实业务中,这类问题是如何逐步暴露出来的
- 在重构过程中,我们做了哪些关键取舍,让模块重新回到"好接入、好理解、也好维护"的状态
这不是一篇讲"如何实现弹窗"的文章,而是一篇关于如何让基础组件在业务中更好用的实践总结。
一个合格的公共组件,应该具备什么特性
设计起点
在开始写此章节前,笔者内心顿生很多疑问,如何在设计公共组件时确定优先级,即何时做窗口场景划分,控制策略应该在第一时间做抽离还是根据业务逐渐演变
设计公共组件时,最容易犯的错误,是一开始就追求完整架构。
例如在 Dialog 模块中,很容易提前抽象出:
- DialogManager
- DialogRequest
- Strategy
- View
但这些模块是否真的需要,并不应该由架构想象决定,而应该由业务语义反推。因此,本文讨论的第一个问题不是"Dialog 模块应该拆成几层",而是:
在真实业务里,弹窗到底承担了哪些不同语义;
从场景出发
如果只是站在抽象层面讨论弹窗,很多判断其实并不容易落地。可一旦回到真实项目里,问题就会立刻具体起来。
这次 dialog 模块服务的并不是符合通用交互的 UI 库,而是针对单一垂直领域设计的弹窗功能基础组件库,业务操作中弹窗出现比较频繁,而且几乎都落在最敏感的用户操作链路上。比如:
- 编辑器页面修改了内容,点击右上角关闭时,需要先确认"是否放弃修改"
- 正在上传或者处理中,业务希望弹出 loading,但又不希望用户随便点掉
- 部分业务分流场景时,需要提供三个出口按钮
- 同一个业务流里,有的弹窗需要返回 bool,有的只负责承载内容,有的则只是单纯阻塞流程 也就是说,真正摆在面前的问题从来不是"能不能弹一个窗",而是这些窗是否能在同一个项目里讲同一种语言。
如何统一语义
在实际项目中,我们会遇到 dialog、popup、content 等多种浮层形态。它们底层大多基于 Overlay 实现,但在业务语义上却存在明显差异:
- 有的弹窗需要返回结果(如确认弹窗)
- 有的只负责展示内容(如提示弹窗)
- 有的用于阻塞流程(如 loading)
如果不加区分地统一为"弹一个窗",最终会导致:
- API 语义混乱
- 调用方无法预期行为
- 不同弹窗之间无法复用
因此,一次弹窗抽象真正需要关注的,不只是 OverlayEntry 如何创建,而是完整链路中每个节点的语义是否清晰:
如果把这个问题抛出来看,一次完整的弹窗逻辑大致如下:
大多数公共组件,并不可能在一开始就设计正确。
尤其是 Dialog 这类强业务语义组件,只有在经历过足够多的真实调用后,才能看清哪些能力是高频需求,哪些只是一次性特例。
重构,是最好的设计
因此,重构并不是简单整理代码,而是重新回答三个问题:
-
哪些能力应该暴露给业务方?
-
哪些能力应该留在模块内部?
-
哪些行为应该通过默认值消化掉?
弹窗语义的三种基本类型
基于行为,我们将弹窗抽象为三类:
-
决策型(Decision)
- 特征:必须用户选择
- 返回值:Future
- 示例:确认弹窗
-
展示型(Display)
- 特征:只展示信息
- 返回值:无
- 示例:Toast / 提示弹窗
-
阻塞型(Blocking)
- 特征:控制流程,不允许随意关闭
- 返回值:由业务控制结束
- 示例:Loading
从语义到设计:我们到底需要什么能力?
当我们将弹窗抽象为三类语义(决策 / 展示 / 阻塞)后,一个更具体的问题就出现了:
👉 组件层需要提供哪些"能力",才能支撑这些语义?
我们可以反推得到几个核心需求:
1. 生命周期控制能力(Lifecycle)
- 什么时候展示?
- 什么时候关闭?
- 是否允许用户主动关闭?
👉 对应:DialogManager
2. 结果传递能力(Result)
- 是否需要返回值?
- 返回值类型如何约束?
👉 对应:Future / DialogRequest
3. 行为控制能力(Behavior)
- 是否允许点击外部关闭?
- 是否阻塞返回?
- 是否支持多按钮分流?
👉 对应:Strategy / Config
4. 渲染能力(View)
- UI 如何组织?
- 是否支持自定义?
👉 对应:DialogView
从可用到可演进
一个公共弹窗组件库,如何才能在复杂业务中长期可用?
模块的划分,本质上不是"设计出来的",而是从语义能力反推出来的
1. 起点:最小可用,而不是一次到位
上文已经提到,一个正常迭代拓展的业务不太可能也不太建议在一开始就定下所有既定的框架结构;
在模块初期,从真实业务场景出发,优先满足:
- 基本弹窗能力(展示 / 确认 / loading)
- 简单的生命周期控制(展示 / 关闭)
- 最基础的参数配置能力
👉 目标只有一个:先让业务用起来
这一步的关键不是"设计多优雅",而是:
❗ 避免过早抽象,允许结构在使用中暴露问题
2. 中期:通过"减法"收敛复杂度
随着使用场景增加,问题开始出现:
- API 逐渐膨胀
- 不同弹窗行为不一致
- 调用方式开始分散
这时,真正重要的不是继续扩展能力,而是做减法:
有哪些公共 API 是必须的,不同组件的默认行为如何设定,这些都要结合实际的场景具体分析;
确切的演进路线
真实情况下往往是先实现再优化,即根据调用方要求的特性完成对应功能实现,然后抽离共性采用模块化集中管理达到整体功能易用性;
可用 => 易维护
举例子,真实业务要实现的并不是可承载一切的弹窗,而是从场景出发的弹窗要求:
- 图标样式设定
- 可操控按钮业务流
- 点击手势操控特性等
并且从实际使用出发,在完成功能实现后还要依据调用方常见传入参数设定默认参数来完成模块将会减少调用方传参心智负担;
避免调用方过度组装底层数据
dart
// 不推荐:调用方组装结构
Dialog.show(
config: DialogConfig(
dismissible: false,
showClose: false,
buttons: [
DialogButton(...),
DialogButton(...),
],
),
);
// 推荐:调用方选择语义
Dialog.confirm(...);
Dialog.loading(...);
Dialog.custom(...);
对于此类需要特定参数完成视图绘制场景,应尽可能抽离设计内部管理参数以及赋值常用参数值,避免在调用时过度进行参数声明;
好的公共组件,不是能力最强,而是让使用者几乎不需要做选择。