Flutter 通用弹窗组件:CommonDialog 一键实现自定义弹窗

弹窗是 Flutter 应用的核心交互组件,广泛用于提示、确认、输入、通知等场景。但原生showDialog配置繁琐,重复编写不仅导致样式不统一,还会产生大量冗余逻辑。本文封装的CommonDialog组件,整合 "单 / 双按钮布局 + 全样式自定义 + 流畅动画 + 灵活交互" 四大核心能力,一行代码即可调用,适配 90%+ 弹窗场景,彻底解放重复开发!

一、核心需求拆解

✅ 布局灵活:支持单按钮(提示类)、双按钮(确认类)两种核心布局,按需切换✅ 样式全自定义:弹窗宽度、圆角、背景色、按钮样式、文本样式均可配置✅ 内容多样化:支持文本、图片、输入框、复选框等任意 Widget 作为弹窗内容✅ 交互可控:支持点击蒙层关闭、禁用蒙层关闭,适配不同业务逻辑✅ 动画流畅:内置淡入淡出 + 缩放动画,避免弹窗 "突兀出现",视觉更自然✅ 易用性强:静态方法直接调用,无需手动构建showDialog,降低使用门槛

二、完整代码实现(可直接复制使用)

dart

复制代码
import 'package:flutter/material.dart';

/// 通用弹窗组件(支持单/双按钮、全自定义样式、灵活交互)
class CommonDialog extends StatelessWidget {
  // 核心配置:标题与内容(可选)
  final String? title; // 弹窗标题(可隐藏)
  final Widget? content; // 弹窗内容(支持任意Widget,如文本、输入框、图片)

  // 按钮配置:单/双按钮自适应
  final String confirmText; // 确认按钮文本(默认"确认")
  final String? cancelText; // 取消按钮文本(null则显示单按钮)
  final VoidCallback onConfirm; // 确认按钮回调(必填)
  final VoidCallback? onCancel; // 取消按钮回调(与cancelText同时存在)

  // 样式配置:全维度自定义
  final double widthRatio; // 弹窗宽度占屏幕比例(默认0.8,适配不同设备)
  final double borderRadius; // 弹窗圆角(默认12px,更符合现代设计)
  final Color bgColor; // 弹窗背景色(默认白色)
  final EdgeInsetsGeometry padding; // 弹窗内边距(默认上下20、左右24)
  final double titleContentSpacing; // 标题与内容间距(默认8px)
  final double contentButtonSpacing; // 内容与按钮间距(默认16px)

  // 文本样式配置
  final TextStyle? titleStyle; // 标题样式(默认18px粗体)
  final TextStyle? contentStyle; // 内容样式(默认16px常规文本)
  final TextStyle? confirmBtnStyle; // 确认按钮文本样式(默认白色16px)
  final TextStyle? cancelBtnStyle; // 取消按钮文本样式(默认黑色87% 16px)

  // 按钮样式配置
  final Color confirmBtnBgColor; // 确认按钮背景色(默认蓝色)
  final Color cancelBtnBgColor; // 取消按钮背景色(默认浅灰)
  final double buttonHeight; // 按钮高度(默认48px)

  // 交互配置
  final bool barrierDismissible; // 点击蒙层是否关闭弹窗(默认true)
  final Color barrierColor; // 蒙层颜色(默认黑色50%透明)

  // 动画配置
  final Duration animationDuration; // 弹窗动画时长(默认200ms)
  final Curve animationCurve; // 动画曲线(默认先慢后快再慢)

  const CommonDialog({
    super.key,
    this.title,
    this.content,
    this.confirmText = "确认",
    this.cancelText,
    required this.onConfirm,
    this.onCancel,
    this.widthRatio = 0.8,
    this.borderRadius = 12.0,
    this.bgColor = Colors.white,
    this.padding = const EdgeInsets.fromLTRB(24, 20, 24, 0),
    this.titleContentSpacing = 8.0,
    this.contentButtonSpacing = 16.0,
    this.titleStyle,
    this.contentStyle,
    this.confirmBtnStyle,
    this.cancelBtnStyle,
    this.confirmBtnBgColor = Colors.blue,
    this.cancelBtnBgColor = const Color(0xFFF5F5F5),
    this.buttonHeight = 48.0,
    this.barrierDismissible = true,
    this.barrierColor = const Color(0x80000000),
    this.animationDuration = const Duration(milliseconds: 200),
    this.animationCurve = Curves.easeInOut,
  }) : assert(
          (cancelText == null && onCancel == null) ||
              (cancelText != null && onCancel != null),
          "取消按钮文本和回调必须同时存在或同时不存在",
        );

  /// 静态调用方法:一行代码显示弹窗(核心易用性优化)
  static void show({
    required BuildContext context,
    String? title,
    Widget? content,
    String confirmText = "确认",
    String? cancelText,
    required VoidCallback onConfirm,
    VoidCallback? onCancel,
    double widthRatio = 0.8,
    double borderRadius = 12.0,
    Color bgColor = Colors.white,
    bool barrierDismissible = true,
  }) {
    showDialog(
      context: context,
      barrierDismissible: barrierDismissible,
      barrierColor: const Color(0x80000000),
      builder: (context) => CommonDialog(
        title: title,
        content: content,
        confirmText: confirmText,
        cancelText: cancelText,
        onConfirm: onConfirm,
        onCancel: onCancel,
        widthRatio: widthRatio,
        borderRadius: borderRadius,
        bgColor: bgColor,
        barrierDismissible: barrierDismissible,
      ),
    );
  }

  /// 构建按钮区域:根据是否有取消按钮,自适应单/双按钮布局
  Widget _buildButtonArea() {
    // 单按钮布局(仅确认按钮)
    if (cancelText == null) {
      return SizedBox(
        width: double.infinity,
        height: buttonHeight,
        child: TextButton(
          onPressed: () {
            Navigator.pop(context); // 关闭弹窗
            onConfirm(); // 执行确认回调
          },
          style: TextButton.styleFrom(
            backgroundColor: confirmBtnBgColor,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(borderRadius),
                bottomRight: Radius.circular(borderRadius),
              ),
            ),
            padding: EdgeInsets.zero,
          ),
          child: Text(
            confirmText,
            style: confirmBtnStyle ??
                const TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                ),
          ),
        ),
      );
    }

    // 双按钮布局(取消+确认)
    return Row(
      children: [
        // 取消按钮
        Expanded(
          child: SizedBox(
            height: buttonHeight,
            child: TextButton(
              onPressed: () {
                Navigator.pop(context); // 关闭弹窗
                onCancel!(); // 执行取消回调(已通过assert保证非空)
              },
              style: TextButton.styleFrom(
                backgroundColor: cancelBtnBgColor,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                    bottomLeft: Radius.circular(borderRadius),
                  ),
                ),
                padding: EdgeInsets.zero,
              ),
              child: Text(
                cancelText!,
                style: cancelBtnStyle ??
                    const TextStyle(
                      color: Colors.black87,
                      fontSize: 16,
                      fontWeight: FontWeight.w500,
                    ),
              ),
            ),
          ),
        ),
        // 确认按钮
        Expanded(
          child: SizedBox(
            height: buttonHeight,
            child: TextButton(
              onPressed: () {
                Navigator.pop(context); // 关闭弹窗
                onConfirm(); // 执行确认回调
              },
              style: TextButton.styleFrom(
                backgroundColor: confirmBtnBgColor,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                    bottomRight: Radius.circular(borderRadius),
                  ),
                ),
                padding: EdgeInsets.zero,
              ),
              child: Text(
                confirmText,
                style: confirmBtnStyle ??
                    const TextStyle(
                      color: Colors.white,
                      fontSize: 16,
                      fontWeight: FontWeight.w500,
                    ),
              ),
            ),
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final dialogWidth = screenWidth * widthRatio; // 自适应屏幕宽度

    // 弹窗核心内容:标题+内容+按钮
    final dialogContent = Column(
      mainAxisSize: MainAxisSize.min, // 仅占用子组件高度,避免弹窗过高
      children: [
        // 标题(可选)
        if (title != null)
          Padding(
            padding: EdgeInsets.only(bottom: titleContentSpacing),
            child: Text(
              title!,
              style: titleStyle ??
                  const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.w600,
                    color: Colors.black87,
                  ),
            ),
          ),
        // 内容(可选)
        if (content != null)
          Padding(
            padding: EdgeInsets.only(bottom: contentButtonSpacing),
            child: content!,
          ),
        // 按钮区域(必选,单/双按钮自适应)
        _buildButtonArea(),
      ],
    );

    // 最终弹窗:包装背景、内边距、圆角、动画
    return AnimatedOpacity(
      opacity: 1.0,
      duration: animationDuration,
      curve: animationCurve,
      child: ScaleTransition(
        scale: Tween<double>(begin: 0.95, end: 1.0).animate(
          CurvedAnimation(
            parent: ModalRoute.of(context)!.animation!,
            curve: animationCurve,
          ),
        ),
        child: Dialog(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(borderRadius),
          ),
          backgroundColor: bgColor,
          barrierColor: barrierColor,
          barrierDismissible: barrierDismissible,
          insetPadding: EdgeInsets.symmetric(
            horizontal: (screenWidth - dialogWidth) / 2,
          ),
          child: Padding(
            padding: padding,
            child: dialogContent,
          ),
        ),
      ),
    );
  }
}

三、实战使用示例(覆盖 5 大高频场景)

场景 1:提示弹窗(单按钮,操作结果反馈)

适配操作成功 / 失败提示、信息告知等场景,简洁明了:

dart

复制代码
// 操作成功提示
CommonDialog.show(
  context: context,
  title: "操作成功",
  content: const Text(
    "您的订单已提交成功,预计3个工作日内发货",
    style: TextStyle(fontSize: 16, color: Color(0xFF666666)),
    textAlign: TextAlign.center,
  ),
  confirmText: "知道了",
  onConfirm: () {
    // 关闭弹窗后执行后续逻辑(如返回上一页)
    Navigator.pop(context);
  },
  borderRadius: 16,
  confirmBtnBgColor: Colors.green,
);

场景 2:确认弹窗(双按钮,危险操作二次确认)

适配删除、退出登录、提交订单等需要二次确认的场景,防止误操作:

dart

复制代码
// 删除数据确认弹窗
CommonDialog.show(
  context: context,
  title: "确认删除",
  content: const Text(
    "确定要删除这条数据吗?删除后不可恢复,请谨慎操作!",
    style: TextStyle(fontSize: 16, color: Colors.redAccent),
    textAlign: TextAlign.center,
  ),
  confirmText: "删除",
  cancelText: "取消",
  onConfirm: () {
    // 执行删除逻辑
    debugPrint("执行数据删除操作");
  },
  onCancel: () {
    // 取消删除,无需额外操作(仅关闭弹窗)
  },
  barrierDismissible: false, // 禁用蒙层关闭,强制用户选择
  confirmBtnBgColor: Colors.redAccent,
  cancelBtnBgColor: const Color(0xFFF0F0F0),
);

场景 3:输入弹窗(带输入框,收集用户信息)

适配备注输入、密码验证、昵称修改等需要用户输入的场景:

dart

复制代码
// 备注输入弹窗
final TextEditingController _remarkController = TextEditingController();

CommonDialog.show(
  context: context,
  title: "输入备注",
  content: TextField(
    controller: _remarkController,
    decoration: const InputDecoration(
      hintText: "请输入备注信息(最多50字)",
      hintStyle: TextStyle(color: Color(0xFF999999)),
      border: OutlineInputBorder(
        borderSide: BorderSide(color: Color(0xFFEEEEEE)),
        borderRadius: BorderRadius.all(Radius.circular(8)),
      ),
      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    ),
    maxLength: 50,
    maxLines: 2,
  ),
  confirmText: "提交",
  cancelText: "取消",
  onConfirm: () {
    // 获取输入的备注内容
    final remark = _remarkController.text.trim();
    debugPrint("用户输入的备注:$remark");
    // 执行提交逻辑
  },
  onCancel: () {
    // 取消输入,释放控制器资源
    _remarkController.dispose();
  },
  widthRatio: 0.85,
  contentButtonSpacing: 20,
);

场景 4:自定义内容弹窗(带图片 + 复选框,活动通知)

适配活动推广、用户协议确认等需要复杂内容的场景:

dart

复制代码
// 活动通知弹窗
CommonDialog.show(
  context: context,
  title: "限时福利活动",
  content: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      // 活动图片
      Image.network(
        "https://example.com/activity-banner.jpg",
        height: 120,
        width: double.infinity,
        fit: BoxFit.cover,
        borderRadius: BorderRadius.circular(8),
      ),
      const SizedBox(height: 12),
      // 活动说明
      const Text(
        "双12限时福利,全场商品8折起,叠加优惠券更划算!",
        style: TextStyle(fontSize: 15, color: Color(0xFF666666)),
      ),
      const SizedBox(height: 12),
      // 复选框(不再提醒)
      Row(
        children: [
          Checkbox(
            value: false,
            onChanged: (value) {
              // 处理复选框状态变化
              debugPrint("不再提醒:${value ?? false}");
            },
            activeColor: Colors.orange,
          ),
          const Text(
            "7天内不再显示",
            style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
          ),
        ],
      ),
    ],
  ),
  confirmText: "立即参与",
  cancelText: "关闭",
  onConfirm: () {
    // 跳转活动页面
    Navigator.pushNamed(context, "/activity");
  },
  widthRatio: 0.9,
  borderRadius: 16,
  confirmBtnBgColor: Colors.orange,
  titleStyle: const TextStyle(color: Colors.orange, fontSize: 20),
);

场景 5:加载中弹窗(无按钮,异步操作等待)

适配接口请求、文件上传等异步操作,显示加载状态:

dart

复制代码
// 加载中弹窗(需手动控制关闭)
void showLoadingDialog(BuildContext context) {
  showDialog(
    context: context,
    barrierDismissible: false, // 禁用蒙层关闭
    builder: (context) => CommonDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const CircularProgressIndicator(strokeWidth: 2, color: Colors.blue),
          const SizedBox(height: 12),
          const Text(
            "加载中...",
            style: TextStyle(fontSize: 16, color: Color(0xFF666666)),
          ),
        ],
      ),
      confirmText: "", // 隐藏按钮
      onConfirm: () {},
      bgColor: Colors.white,
      borderRadius: 12,
      padding: const EdgeInsets.symmetric(vertical: 24),
    ),
  );
}

// 使用方式:接口请求前显示,请求后关闭
showLoadingDialog(context);
// 模拟接口请求
await Future.delayed(const Duration(seconds: 2));
Navigator.pop(context); // 关闭加载弹窗

四、核心封装技巧(让组件更易用、更稳定)

  1. 静态调用简化流程 :通过static void show方法封装showDialog,无需手动构建弹窗实例,一行代码即可调用,降低使用门槛。
  2. 布局自适应设计 :弹窗宽度按屏幕比例(widthRatio)计算,适配手机、平板等不同设备;MainAxisSize.min确保弹窗仅占用必要高度,避免拉伸。
  3. 样式优先级清晰 :自定义样式(如titleStyleconfirmBtnStyle)优先级高于默认样式,既保证通用性,又支持个性化定制。
  4. 动画双重优化 :组合AnimatedOpacity(淡入)和ScaleTransition(缩放)动画,模拟原生弹窗的 "弹起" 效果,视觉更自然。
  5. 交互安全可控 :通过assert校验取消按钮的文本和回调一致性,避免空指针异常;支持禁用蒙层关闭,适配危险操作场景。
  6. 内容高度灵活content参数支持任意 Widget,可轻松实现输入框、图片、复选框等复杂内容,无需修改组件源码。

五、避坑指南(实际开发必看)

  1. 按钮回调非空校验 :双按钮模式下,cancelTextonCancel必须同时传入,组件已通过assert校验,开发时需注意配对使用。
  2. 蒙层关闭逻辑barrierDismissible: false时,用户无法通过点击蒙层关闭弹窗,需确保弹窗有明确的关闭按钮(如取消、确认),避免用户陷入操作僵局。
  3. 自定义内容尺寸控制 :当content为输入框、图片等组件时,建议设置明确的尺寸或约束(如maxLinesheight),避免弹窗内容溢出。
  4. 控制器资源释放 :若弹窗包含TextField,需在onCancelonConfirm中释放TextEditingController,避免内存泄漏。
  5. 弹窗叠加问题 :避免短时间内连续调用show方法,若需连续显示多个弹窗,建议通过Future等待前一个弹窗关闭后再显示下一个。

总结

CommonDialog组件通过 "通用化封装 + 精细化控制",彻底解决了原生弹窗开发的痛点,实现了 "一行代码调用、全场景适配、样式统一" 的目标。无论是简单的提示弹窗,还是复杂的输入、活动弹窗,都能通过灵活配置快速实现,既提升了开发效率,又保证了 APP 内弹窗交互的一致性。

https://openharmonycrossplatform.csdn.net/content

相关推荐
Non-existent9877 小时前
Flutter + FastAPI 30天速成计划自用并实践-第6天
flutter·fastapi
解局易否结局7 小时前
Flutter:重塑跨平台开发的生态与实践
flutter
Android_Trot8 小时前
Flutter android 多渠道配置,多包名、icon、等配置。
android·flutter
淡写成灰9 小时前
Flutter PopScope 返回拦截完整指南
flutter
ujainu9 小时前
Flutter与DevEco Studio协同开发:HarmonyOS应用实战指南
flutter·华为·harmonyos
赵财猫._.10 小时前
【Flutter x 鸿蒙】第四篇:双向通信——Flutter调用鸿蒙原生能力
flutter·华为·harmonyos
解局易否结局10 小时前
Flutter:跨平台开发的范式革新与实践之道
flutter
解局易否结局11 小时前
Flutter:跨平台开发的革命与实战指南
flutter