unified_popups
📖 概述
Unified Popups 是一个专为企业级 Flutter 应用设计的统一弹窗解决方案。它提供了简洁、易用的 API,覆盖了常见的弹窗场景,包括轻提示、加载指示器、确认对话框、底部面板、日期选择器和锚定菜单等。
✨ 核心特性
- 🆕 异步弹框支持:所有弹窗类型均支持在异步方法中调用,无需担心构建阶段错误。基于
SafeOverlayEntry和构建阶段检测机制,自动检测构建阶段并延迟执行,完美支持Future.then()、async/await、Stream、Timer、build()方法中直接调用等所有场景 - 统一 API:所有弹窗通过
Pop静态类调用,API 设计简洁一致 - 类型安全:依托 Dart 强类型系统与
flutter_lints规则,编译期即可发现绝大多数问题 - 多实例支持:基于 Overlay 实现,支持同时显示多个弹窗
- 动画可塑性:每个 API 都支持自定义动画时长和动画曲线,快速适配不同交互语境
- 键盘适配:自动处理键盘弹出时的布局调整和焦点管理
- 手势支持:支持拖拽关闭、点击遮罩关闭等交互
- 主题化:支持自定义样式和主题配置
- 无障碍支持:内置可访问性支持,符合无障碍设计标准
- 性能优化:基于 Overlay 实现,性能优异,内存占用低
🎯 适用场景
- 企业级应用中的各种弹窗需求
- 需要统一弹窗体验的大型项目
- 对键盘适配和用户体验有高要求的应用
- 需要支持多端(移动端、Web、桌面端)的项目
🚀 快速开始
安装
在 pubspec.yaml 中添加依赖:
dependencies:
unified_popups: ^1.1.14 # 选择最新版本
初始化
在 main.dart 中初始化:
import 'package:unified_popups/unified_popups.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const MyApp());
// 确保 MaterialApp 构建完毕后,再初始化 PopupManager
WidgetsBinding.instance.addPostFrameCallback((_) {
PopupManager.initialize(navigatorKey: navigatorKey);
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: GlobalKey<NavigatorState>(), // 必须提供
home: PopScopeWidget( // 可选:用于处理返回键
child: HomePage(),
),
);
}
}
基本使用
// 显示轻提示
Pop.toast('操作成功', toastType: ToastType.success);
// 显示加载指示器
Pop.loading(message: '加载中...');
// ... 异步操作
Pop.hideLoading();
// 显示确认对话框
final result = await Pop.confirm(
title: '确认删除',
content: '此操作不可撤销,是否继续?',
confirmText: '删除',
cancelText: '取消',
confirmBorder: Border.all(color: Colors.redAccent),
);
📚 API 参考
Toast 轻提示
用于显示临时性的消息提示。
Pop.toast(
String? message, {
PopupPosition position = PopupPosition.center,
Duration duration = const Duration(milliseconds: 1200),
bool showBarrier = false,
bool barrierDismissible = false,
ToastType toastType = ToastType.none,
Duration animationDuration = const Duration(milliseconds: 200),
Curve? animationCurve,
String? customImagePath,
double? imageSize,
Color? imgColor,
Axis layoutDirection = Axis.horizontal,
EdgeInsetsGeometry? padding,
EdgeInsetsGeometry? margin,
Decoration? decoration,
TextStyle? style,
TextAlign? textAlign,
Widget? messageWidget,
String? tMessage,
String? tImagePath,
ToastType? tToastType,
Color? tImgColor,
VoidCallback? onTap,
bool toggleable = false,
})
参数说明:
message:消息文本(可选,与messageWidget二选一)messageWidget:自定义消息 Widget(可选),如果提供则优先使用,忽略messageposition:显示位置,支持top、center、bottom、left、rightduration:显示时长,默认 1.2 秒toastType:提示类型,支持success、warn、error、noneanimationDuration:动画持续时间,默认 200msanimationCurve:动画曲线,默认Curves.easeInOutcustomImagePath:自定义图片路径,如果提供则覆盖 toastType 的图标imageSize:图片大小,默认 24.0imgColor:自定义图片的着色,仅在提供customImagePath时生效layoutDirection:布局方向,默认Axis.horizontal(Row),Axis.vertical为 Column(图片在上,文字在下)showBarrier:是否显示遮罩层barrierDismissible:点击遮罩是否关闭tMessage:切换后的消息文本(可选),用于切换模式tImagePath:切换后的自定义图片路径(可选),用于切换模式tToastType:切换后的 Toast 等级(可选),用于切换模式tImgColor:切换后的自定义图片的着色(可选),用于切换模式onTap:点击回调(可选)toggleable:是否可切换,默认false。当设置为true且提供了tMessage或tImagePath时,点击 toast 会在两个状态间切换
使用示例:
// 成功提示
Pop.toast('保存成功', toastType: ToastType.success);
// 错误提示
Pop.toast('网络异常,请稍后重试', toastType: ToastType.error);
// 自定义图片
Pop.toast(
'自定义图片提示',
customImagePath: 'assets/custom_icon.png',
imageSize: 32.0,
imgColor: Colors.orange,
layoutDirection: Axis.vertical, // 图片在上,文字在下
);
// 自定义动画时长
Pop.toast('快速提示', animationDuration: Duration(milliseconds: 100));
// 自定义样式
Pop.toast(
'自定义样式提示',
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
borderRadius: BorderRadius.circular(20),
),
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
);
// 切换模式:平衡锁定和重力感应
Pop.toast(
'平衡锁定',
customImagePath: 'assets/balance_lock.png',
tMessage: '重力感应',
tImagePath: 'assets/gravity.png',
toggleable: true,
imageSize: 32,
duration: const Duration(seconds: 2),
onTap: () {
print('Toast 状态已切换');
},
);
// Widget 自定义消息
Pop.toast(
messageWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: 8),
Text('操作成功', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
);
Loading 加载指示器
用于显示加载状态,支持自定义样式和交互。
void loading({
String? message,
Color? backgroundColor,
double? borderRadius,
Color? indicatorColor,
double? indicatorStrokeWidth,
TextStyle? textStyle,
Widget? customIndicator,
Duration rotationDuration = const Duration(seconds: 1),
bool showBarrier = true,
bool barrierDismissible = false,
Color barrierColor = Colors.black54,
Duration animationDuration = const Duration(milliseconds: 150),
Curve? animationCurve,
})
注意: 整个应用同时只能有一个 loading,调用此方法会自动关闭之前的 loading(如果存在)。不需要手动管理 loading ID。
参数说明:
customIndicator:自定义 Widget(通常是图片),如果提供则替代默认的 CircularProgressIndicator,并自动添加旋转动画rotationDuration:旋转动画持续时间,默认 1 秒。仅在使用 customIndicator 时生效animationCurve:动画曲线,默认Curves.easeInOut
使用示例:
// 基本使用
Pop.loading(message: '提交中...');
await submitData();
Pop.hideLoading();
// 使用自定义图片作为 loading 图标
Pop.loading(
message: '加载中',
customIndicator: Image.asset('assets/loading.png'),
rotationDuration: Duration(milliseconds: 800),
);
// 自定义样式
Pop.loading(
message: '自定义样式 Loading',
backgroundColor: Colors.purple.withOpacity(0.9),
borderRadius: 20,
indicatorColor: Colors.white,
indicatorStrokeWidth: 3,
textStyle: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
);
// 快速显示 Loading
Pop.loading(
message: '快速加载',
animationDuration: Duration(milliseconds: 100),
);
Confirm 确认对话框
用于需要用户确认的操作,支持丰富的自定义选项。
Future<bool?> confirm({
String? title,
Widget? titleWidget,
String? content,
Widget? contentWidget,
PopupPosition position = PopupPosition.center,
String? confirmText,
Widget? confirmButtonWidget,
String? cancelText,
Widget? cancelButtonWidget,
bool showCloseButton = true,
TextStyle? titleStyle,
TextStyle? contentStyle,
TextStyle? confirmStyle,
TextStyle? cancelStyle,
String? imagePath,
double? imageHeight = 80,
double? imageWidth,
TextAlign? textAlign = TextAlign.center,
ConfirmButtonLayout? buttonLayout = ConfirmButtonLayout.row,
BorderRadiusGeometry? buttonBorderRadius,
BoxBorder? confirmBorder,
BoxBorder? cancelBorder,
Color? confirmBgColor,
Color? cancelBgColor,
EdgeInsetsGeometry? padding,
EdgeInsetsGeometry? margin,
Decoration? decoration,
Widget? confirmChild,
VoidCallback? onConfirm,
VoidCallback? onCancel,
Duration animationDuration = const Duration(milliseconds: 250),
Curve? animationCurve,
})
新增参数亮点:
titleWidget/contentWidget/confirmButtonWidget/cancelButtonWidget:支持完全自定义标题、内容和按钮 Widget,优先于对应的 String 参数onConfirm/onCancel:按钮点击回调,在内部关闭逻辑之前执行,允许外部完全接管按钮点击事件confirmBorder/cancelBorder:允许自定义按钮边框样式animationCurve:可自定义进出场曲线,默认Curves.easeInOut
返回值:
true:用户点击确认false:用户点击取消null:用户点击遮罩或关闭按钮
使用示例:
// 基本确认对话框
final result = await Pop.confirm(
title: '删除确认',
content: '删除后将不可恢复,是否继续?',
confirmText: '删除',
cancelText: '取消',
);
// 带输入框的确认对话框
final result = await Pop.confirm(
title: '输入信息',
content: '请填写以下信息:',
confirmChild: Column(
children: [
TextField(decoration: InputDecoration(labelText: '姓名')),
TextField(decoration: InputDecoration(labelText: '邮箱')),
],
),
);
// 危险操作确认
final result = await Pop.confirm(
title: '危险操作',
content: '此操作不可撤销!',
confirmText: '删除',
confirmBgColor: Colors.red,
confirmBorder: Border.all(color: Colors.redAccent),
cancelBorder: Border.all(color: Colors.redAccent.withOpacity(0.3)),
buttonLayout: ConfirmButtonLayout.column,
);
// 快速确认对话框
final result = await Pop.confirm(
title: '快速确认',
content: '快速确认操作',
animationDuration: Duration(milliseconds: 150),
);
// Widget 自定义 Confirm
final result = await Pop.confirm(
titleWidget: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text('Widget 标题'),
],
),
contentWidget: Column(
children: [
Text('这是自定义内容 Widget'),
SizedBox(height: 8),
Icon(Icons.info, color: Colors.blue),
],
),
confirmButtonWidget: Container(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check, color: Colors.white),
SizedBox(width: 4),
Text('确认', style: TextStyle(color: Colors.white)),
],
),
),
cancelButtonWidget: Text('取消', style: TextStyle(color: Colors.grey)),
onConfirm: () {
print('确认按钮被点击');
},
onCancel: () {
print('取消按钮被点击');
},
);
Sheet 底部面板
用于显示从指定方向滑出的面板,常用于列表选择、表单填写等场景。
Future<T?> sheet<T>({
required Widget Function(void Function([T? result]) dismiss) childBuilder,
String? title,
Widget? titleWidget,
SheetDirection direction = SheetDirection.bottom,
bool showCloseButton = false,
bool? useSafeArea,
SheetDimension? width,
SheetDimension? height,
SheetDimension? maxWidth,
SheetDimension? maxHeight,
bool? showBarrier,
bool? barrierDismissible,
Color? barrierColor,
String? imgPath,
Color? backgroundColor,
BorderRadius? borderRadius,
List<BoxShadow>? boxShadow,
EdgeInsetsGeometry? padding,
EdgeInsetsGeometry? titlePadding,
TextStyle? titleStyle,
TextAlign? titleAlign,
bool dockToEdge = false,
double? edgeGap,
Duration animationDuration = const Duration(milliseconds: 400),
Curve? animationCurve,
})
参数说明:
childBuilder:内容构建器,接收dismiss函数用于关闭面板title:标题文本(可选,与titleWidget二选一)titleWidget:自定义标题 Widget(可选),如果提供则优先使用,忽略titledirection:滑出方向,支持top、bottom、left、rightwidth/height:尺寸,支持像素值和百分比useSafeArea:是否使用安全区域showBarrier/barrierDismissible/barrierColor:控制遮罩层是否显示、是否可点击关闭以及遮罩颜色dockToEdge:在bottom/left/right方向弹出时,是否保留原边缘的交互区域(遮罩和内容都会避开该区域)edgeGap:保留边缘区域的尺寸,默认kBottomNavigationBarHeight + 4animationDuration:动画持续时间,默认 400msanimationCurve:动画曲线,默认Curves.easeInOut
dockToEdge不支持top方向,启用后留白区域可透传到底部/侧边的 TabBar 或导航组件。
使用示例:
// 底部选择面板
final result = await Pop.sheet<String>(
title: '选择操作',
childBuilder: (dismiss) => ListView(
children: [
ListTile(
title: Text('复制'),
onTap: () => dismiss('copy'),
),
ListTile(
title: Text('删除'),
onTap: () => dismiss('delete'),
),
],
),
);
// 表单面板
await Pop.sheet<void>(
title: '用户信息',
childBuilder: (dismiss) => Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(decoration: InputDecoration(labelText: '姓名')),
TextField(decoration: InputDecoration(labelText: '邮箱')),
ElevatedButton(
onPressed: () => dismiss(),
child: Text('提交'),
),
],
),
),
);
// 自定义样式面板
await Pop.sheet<void>(
title: '自定义样式',
backgroundColor: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
childBuilder: (dismiss) => Container(
padding: EdgeInsets.all(16),
child: Text('自定义内容'),
),
);
// 自定义遮罩交互
await Pop.sheet<void>(
title: '不可点击关闭',
showBarrier: true,
barrierDismissible: false,
barrierColor: Colors.black87.withOpacity(0.6),
childBuilder: (dismiss) => Container(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () => dismiss(),
child: Text('关闭'),
),
),
);
// TabBar 顶部弹出,保留底部导航
await Pop.sheet<void>(
title: 'TabBar 顶部弹出',
dockToEdge: true,
edgeGap: 64, // 可选:自定义保留高度 / 宽度
childBuilder: (dismiss) => ListView(
shrinkWrap: true,
children: [
ListTile(title: Text('收藏'), onTap: () => dismiss()),
ListTile(title: Text('分享'), onTap: () => dismiss()),
],
),
);
// TabBar 保持可点,遮罩和内容都会停在 TabBar 顶部
// 快速面板
await Pop.sheet<void>(
title: '快速面板',
animationDuration: Duration(milliseconds: 200),
childBuilder: (dismiss) => Container(
padding: EdgeInsets.all(16),
child: Text('快速内容'),
),
);
// Widget 自定义标题
await Pop.sheet<void>(
titleWidget: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.settings, color: Colors.blue),
SizedBox(width: 8),
Text('设置面板', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
childBuilder: (dismiss) => Container(
padding: EdgeInsets.all(16),
child: Text('自定义标题内容'),
),
);
Date 日期选择器
用于日期选择,支持自定义范围和样式。
Future<DateTime?> date({
DateTime? initialDate,
DateTime? minDate,
DateTime? maxDate,
String title = 'Date of Birth',
PopupPosition position = PopupPosition.bottom,
String confirmText = 'Confirm',
String? cancelText = 'Cancel',
Color? activeColor = Colors.black,
Color? noActiveColor = Colors.black38,
Color? headerBg = Colors.blue,
double? height = 180.0,
double? radius = 24.0,
Duration animationDuration = const Duration(milliseconds: 250),
Curve? animationCurve,
})
可通过 animationDuration 与 animationCurve 调整日期弹窗的节奏与缓动。
使用示例:
// 基本日期选择
final date = await Pop.date(
title: '选择生日',
minDate: DateTime(1900, 1, 1),
maxDate: DateTime.now(),
);
// 自定义样式
final date = await Pop.date(
title: '选择入职日期',
initialDate: DateTime.now(),
minDate: DateTime(2020, 1, 1),
maxDate: DateTime.now(),
confirmText: '确定',
cancelText: '取消',
activeColor: Colors.green,
headerBg: Colors.green,
height: 200,
radius: 16,
);
// 快速日期选择
final date = await Pop.date(
title: '快速选择日期',
animationDuration: Duration(milliseconds: 150),
);
Menu 锚定菜单
用于在指定组件附近显示菜单或气泡。
Future<T?> menu<T>({
required GlobalKey anchorKey,
Offset anchorOffset = Offset.zero,
required Widget Function(void Function([T? result]) dismiss) builder,
bool showBarrier = true,
bool barrierDismissible = true,
Color? barrierColor,
PopupAnimation animation = PopupAnimation.fade,
Duration animationDuration = const Duration(milliseconds: 200),
BoxDecoration? decoration,
EdgeInsetsGeometry? padding,
BoxConstraints? constraints,
Curve? animationCurve,
})
锚定菜单默认使用半透明遮罩(Colors.black54)。可通过 barrierColor、animationDuration、animationCurve 和 PopupAnimation 组合出合适的动效。
使用示例:
// 基本菜单
final GlobalKey buttonKey = GlobalKey();
ElevatedButton(
key: buttonKey,
onPressed: () async {
final result = await Pop.menu<String>(
anchorKey: buttonKey,
anchorOffset: Offset(0, 8),
builder: (dismiss) => Column(
children: [
ListTile(
title: Text('复制'),
onTap: () => dismiss('copy'),
),
ListTile(
title: Text('删除'),
onTap: () => dismiss('delete'),
),
],
),
);
},
child: Text('显示菜单'),
);
// 带图标的菜单
final result = await Pop.menu<String>(
anchorKey: buttonKey,
builder: (dismiss) => Column(
children: [
ListTile(
leading: Icon(Icons.copy),
title: Text('复制'),
onTap: () => dismiss('copy'),
),
ListTile(
leading: Icon(Icons.delete, color: Colors.red),
title: Text('删除', style: TextStyle(color: Colors.red)),
onTap: () => dismiss('delete'),
),
],
),
);
🎨 样式定制
动画时长与曲线配置
每个弹窗 API 都支持自定义动画时长与动画曲线,为不同场景提供最佳的用户体验:
// 快速反馈场景
Pop.toast('快速提示', animationDuration: Duration(milliseconds: 100));
Pop.loading(message: '快速加载', animationDuration: Duration(milliseconds: 100));
// 重要操作场景
Pop.confirm(
title: '危险操作',
content: '此操作不可撤销!',
animationDuration: Duration(milliseconds: 300), // 稍慢,给用户思考时间
);
// 复杂内容场景
Pop.sheet(
title: '复杂操作',
animationDuration: Duration(milliseconds: 500), // 较长动画,适合复杂内容
animationCurve: Curves.easeOutCubic,
childBuilder: (dismiss) => ComplexWidget(),
);
// 自定义曲线
Pop.menu(
anchorKey: buttonKey,
animationCurve: Curves.easeOutBack,
builder: (dismiss) => const MenuBody(),
);
默认动画时长:
Pop.toast(): 200ms (快速显示)Pop.loading(): 150ms (快速显示)Pop.confirm(): 250ms (适中时长)Pop.date(): 250ms (适中时长)Pop.menu(): 200ms (快速响应)Pop.sheet(): 400ms (较长动画,适合抽屉效果)
所有 API 的默认动画曲线均为 Curves.easeInOut,通过 animationCurve 可替换为更合适的缓动函数。
全局样式配置
可以通过自定义主题来统一配置弹窗样式:
// 在 MaterialApp 中配置主题
MaterialApp(
theme: ThemeData(
// 自定义弹窗样式
dialogTheme: DialogTheme(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
)
局部样式定制
每个弹窗 API 都支持局部样式定制:
// 自定义 Toast 样式
Pop.toast(
'自定义样式',
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
borderRadius: BorderRadius.circular(20),
),
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
);
// 自定义 Confirm 样式
Pop.confirm(
title: '自定义样式',
content: '内容',
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.teal, Colors.white]),
borderRadius: BorderRadius.circular(24),
),
titleStyle: TextStyle(color: Colors.blue, fontSize: 20),
confirmBgColor: Colors.green,
cancelBgColor: Colors.pink,
);
🔧 最佳实践
1. 键盘适配
当弹窗包含输入框时,库会自动处理键盘弹出:
// 推荐:使用 confirmChild 添加输入框
Pop.confirm(
title: '输入信息',
content: '请填写以下信息:',
confirmChild: Column(
children: [
TextField(decoration: InputDecoration(labelText: '姓名')),
TextField(decoration: InputDecoration(labelText: '邮箱')),
],
),
);
// 推荐:在 Sheet 中使用 ListView 处理长内容
Pop.sheet(
childBuilder: (dismiss) => ListView(
children: [
TextField(decoration: InputDecoration(labelText: '字段1')),
TextField(decoration: InputDecoration(labelText: '字段2')),
// ... 更多字段
],
),
);
2. 错误处理
// 推荐:使用 try-catch 处理异步操作
try {
final result = await Pop.confirm(
title: '确认操作',
content: '是否继续?',
);
if (result == true) {
await performOperation();
Pop.toast('操作成功', toastType: ToastType.success);
}
} catch (e) {
Pop.toast('操作失败: $e', toastType: ToastType.error);
}
3. 加载状态管理
// 推荐:使用 Loading 包装异步操作
Future<void> submitForm() async {
Pop.loading(message: '提交中...');
try {
await api.submit(formData);
Pop.hideLoading();
Pop.toast('提交成功', toastType: ToastType.success);
} catch (e) {
Pop.hideLoading();
Pop.toast('提交失败: $e', toastType: ToastType.error);
}
}
4. 返回键处理
// 推荐:使用 PopScopeWidget 包装应用
MaterialApp(
home: PopScopeWidget(
child: HomePage(),
),
);
// 或者手动处理返回键
WillPopScope(
onWillPop: () async {
if (PopupManager.hasNonToastPopup) {
PopupManager.hideLastNonToast();
return false; // 阻止页面返回
}
return true; // 允许页面返回
},
child: HomePage(),
)
5. 路由切换与自动清理
PopScopeWidget 负责拦截系统返回键,而 PopupRouteObserver 则在路由 push/pop/replace 时主动关闭需要清理的弹窗(如 confirm 与 sheet),两者协同可覆盖绝大多数导航场景:
final navigatorKey = GlobalKey<NavigatorState>();
MaterialApp(
navigatorKey: navigatorKey,
home: const PopScopeWidget(child: HomePage()),
navigatorObservers: const [PopupRouteObserver()],
);
- confirm、sheet 默认会在路由切换时关闭,toast/loading 仍保持显示
- 若需要覆写行为,可在创建弹窗时通过
PopupConfig.dismissOnRouteChange指定 - 路由切换关闭的是所有匹配弹窗;返回键只会关闭最近的非 Toast 弹窗,不会影响路由栈
🔧 PopupManager 原理与 popupId 使用规则
核心原理
PopupManager 是一个单例模式的弹窗管理器,基于 Flutter 的 Overlay 系统实现。它的核心工作原理如下:
1. 弹窗生命周期管理
每个弹窗都有唯一的 popupId,用于标识和管理:
// 弹窗 ID 生成规则
final popupId = 'popup_${DateTime.now().microsecondsSinceEpoch}_${_instance._popups.length}';
2. 内部数据结构
class _PopupInfo {
final OverlayEntry entry; // UI 组件
final AnimationController controller; // 动画控制器
final VoidCallback? onDismissCallback; // 关闭回调
final PopupType type; // 弹窗类型
Timer? dismissTimer; // 自动关闭定时器
}
3. 弹窗显示流程
- 创建资源:为每个弹窗创建独立的
AnimationController和OverlayEntry - 存储信息:将弹窗信息存储在
_popupsMap 中,按显示顺序记录在_popupOrder列表中 - 插入 UI:将
OverlayEntry插入到 Flutter 的 Overlay 系统中 - 播放动画:执行进入动画,完成后触发
onShow回调 - 启动定时器:如果设置了
duration,启动自动关闭定时器
4. 弹窗关闭流程
- 立即移除:从管理器中移除弹窗信息,防止重复关闭
- 取消定时器:取消可能存在的自动关闭定时器
- 播放退出动画:执行退出动画
- 清理资源:动画完成后移除
OverlayEntry并释放AnimationController - 触发回调:调用
onDismiss回调
popupId 使用规则
✅ 可以通过 popupId 关闭的弹窗
1. Loading 弹窗
// Loading 现在不需要 ID,内部自动管理
Pop.loading(message: '加载中...');
// ... 异步操作
Pop.hideLoading(); // ✅ 可以关闭,不需要参数
2. 手动创建的弹窗
// 通过 PopupManager.show() 直接创建的弹窗
final popupId = PopupManager.show(PopupConfig(
child: CustomWidget(),
// ... 其他配置
));
PopupManager.hide(popupId); // ✅ 可以关闭
❌ 不能通过 popupId 关闭的弹窗
1. Toast 弹窗
// Toast 不返回 popupId,自动管理生命周期
Pop.toast('消息'); // ❌ 无法通过 popupId 关闭
2. Confirm 弹窗
// Confirm 通过用户交互关闭,不返回 popupId
await Pop.confirm(content: '确认?'); // ❌ 无法通过 popupId 关闭
3. Sheet 弹窗
// Sheet 通过 dismiss() 函数关闭,不返回 popupId
await Pop.sheet(childBuilder: (dismiss) => ...); // ❌ 无法通过 popupId 关闭
4. Date 弹窗
// Date 通过用户选择关闭,不返回 popupId
await Pop.date(); // ❌ 无法通过 popupId 关闭
5. Menu 弹窗
// Menu 通过 dismiss() 函数关闭,不返回 popupId
await Pop.menu(builder: (dismiss) => ...); // ❌ 无法通过 popupId 关闭
全局管理方法
1. 隐藏最后一个弹窗
// 隐藏最新显示的弹窗(任何类型)
PopupManager.hideLast();
2. 隐藏所有弹窗
// 隐藏所有当前显示的弹窗
PopupManager.hideAll();
3. 隐藏最后一个非 Toast 弹窗
// 隐藏最后一个非 Toast 类型的弹窗
PopupManager.hideLastNonToast();
4. 根据类型隐藏弹窗
// 隐藏指定类型的弹窗(从最新的开始查找)
PopupManager.hideByType(PopupType.loading);
PopupManager.hideByType(PopupType.toast);
// 主要用于单一实例的弹窗类型,如 loading
4. 检查弹窗状态
// 检查指定 ID 的弹窗是否可见
bool isVisible = PopupManager.isVisible(popupId);
// 检查是否有非 Toast 弹窗
bool hasPopup = PopupManager.hasNonToastPopup;
5. 智能返回处理
// 如果有弹窗则关闭弹窗,否则执行页面返回
PopupManager.maybePop(context);
最佳实践
1. Loading 弹窗管理
// 现在不需要手动管理 loading ID,直接调用即可
class LoadingManager {
static void show(String message) {
Pop.loading(message: message);
}
static void hide() {
Pop.hideLoading();
}
}
2. 返回键处理
// 使用 PopScopeWidget 自动处理返回键
MaterialApp(
home: PopScopeWidget(
child: HomePage(),
),
);
// 或手动处理
WillPopScope(
onWillPop: () async {
if (PopupManager.hasNonToastPopup) {
PopupManager.hideLastNonToast();
return false; // 阻止页面返回
}
return true; // 允许页面返回
},
child: HomePage(),
)
3. PopScopeWidget 与 PopupManager 搭配使用
// 示例:在示例工程中查看 PopupManagerPage 获取完整示例
// 展示如何使用 PopScopeWidget 和 PopupManager.show 进行弹窗管理
// 使用 PopScopeWidget 包装页面
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopScopeWidget(
child: Scaffold(
appBar: AppBar(
title: Text('弹窗管理示例'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
// 智能处理返回:有弹窗则关闭弹窗,否则返回上一页
PopupManager.maybePop(context);
},
),
),
body: Column(
children: [
ElevatedButton(
onPressed: () {
// 使用 PopupManager.show 直接创建自定义弹窗
PopupManager.show(
PopupConfig(
child: Container(
padding: EdgeInsets.all(20),
child: Text('自定义弹窗'),
),
animation: PopupAnimation.fade,
),
);
},
child: Text('显示自定义弹窗'),
),
],
),
),
);
}
}
4. 错误处理
// 确保 Loading 在异常情况下也能被关闭
Future<void> safeOperation() async {
try {
Pop.loading(message: '处理中...');
await riskyOperation();
Pop.hideLoading();
Pop.toast('成功', toastType: ToastType.success);
} catch (e) {
Pop.hideLoading();
Pop.toast('失败: $e', toastType: ToastType.error);
}
}
🚀 性能优化
1. 避免频繁创建弹窗
// 不推荐:频繁创建相同弹窗
for (int i = 0; i < 100; i++) {
Pop.toast('消息 $i');
}
// 推荐:批量处理或使用节流
Timer? _toastTimer;
void showToast(String message) {
_toastTimer?.cancel();
_toastTimer = Timer(Duration(milliseconds: 100), () {
Pop.toast(message);
});
}
2. 合理使用 Loading
// 推荐:为长时间操作显示 Loading
Future<void> longOperation() async {
Pop.loading(message: '处理中...');
try {
await Future.delayed(Duration(seconds: 3)); // 模拟长时间操作
Pop.hideLoading();
Pop.toast('操作完成', toastType: ToastType.success);
} catch (e) {
Pop.hideLoading();
Pop.toast('操作失败', toastType: ToastType.error);
}
}
🔮 更新建议
短期计划
-
动画增强
- 支持更多动画类型(弹性、缓动等)
- 自定义动画曲线
- 动画时长配置 ✅ 已完成
-
主题系统
- 全局主题配置
- 暗色模式支持
- 主题切换动画
-
无障碍优化
- 屏幕阅读器支持
- 键盘导航支持
- 焦点管理优化
中期计划
-
新组件
- 进度条弹窗
- 图片预览弹窗
- 文件选择弹窗
- 颜色选择器
-
交互增强
- 拖拽排序
- 手势识别
- 多点触控支持
-
性能优化
- 虚拟滚动支持
- 懒加载优化
- 内存使用优化
长期计划
-
平台扩展
- Web 端优化
- 桌面端支持
- 移动端原生体验
-
生态系统
- 插件系统
- 第三方组件库
- 社区贡献指南
-
企业级功能
- 多语言支持
- 权限控制
- 审计日志
🐛 常见问题
Q: 如何自定义弹窗位置?
A: 使用 position 参数或 anchorKey 进行定位:
// 使用预设位置
Pop.toast('消息', position: PopupPosition.bottom);
// 使用锚定定位
final GlobalKey key = GlobalKey();
Pop.menu(anchorKey: key, builder: (dismiss) => ...);
Q: 如何处理键盘弹出?
A: 库会自动处理键盘适配,但建议使用 confirmChild 或 ListView:
// 推荐方式
Pop.confirm(
confirmChild: TextField(decoration: InputDecoration(labelText: '输入')),
);
// 或使用 Sheet
Pop.sheet(
childBuilder: (dismiss) => ListView(
children: [TextField(...)],
),
);
Q: 如何实现全局弹窗管理?
A: 使用 PopupManager 进行全局管理:
// 隐藏所有弹窗
PopupManager.hideAll();
// 隐藏最后一个非 Toast 弹窗
PopupManager.hideLastNonToast();
// 检查是否有弹窗
if (PopupManager.hasNonToastPopup) {
// 处理返回键
}
Q: 如何自定义动画时长?
A: 使用 animationDuration 参数:
// 快速显示
Pop.toast('快速提示', animationDuration: Duration(milliseconds: 100));
// 慢速显示
Pop.sheet(
childBuilder: (dismiss) => YourWidget(),
animationDuration: Duration(milliseconds: 600),
);
📄 许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。
📚 更多文档
Unified Popups - 让弹窗开发更简单、更统一、更高效!