Flutter 弹窗体系重构:为什么我们抛弃 showDialog、直面 BaseModalRoute

概述

Flutter 自带的 showDialogshowModalBottomSheet 确实能“把东西弹出来”,但在复杂项目里,它们很快就暴露出三个痛点:

  • 动效/遮罩写死,做不到“像原生一样”;
  • push/pop、埋点、日志被打散在业务代码里,难维护;
  • 想再封装成复用组件?要么复制 ModalRoute,要么手撸 OverlayEntry,都很费劲。

于是我们决定自建一套骨架:BaseModalRoute。它专门解决这三个问题,又保留高度扩展性。


1. BaseModalRoute:所有弹窗的“底盘”

  • 统一遮罩控制,ModalBarrierConfig 表述颜色/可否点击关闭等。
  • 统一生命周期日志,方便排查 push/pop。
  • 支持注入自定义 transitionBuilder,默认提供淡入 + 缩放。
typedef ModalTransitionBuilder = Widget Function(
  BuildContext context,
  Animation<double> animation,
  Animation<double> secondaryAnimation,
  Widget child,
);

abstract class BaseModalRoute<T> extends ModalRoute<T> {
  BaseModalRoute({
    required this.barrier,
    this.transitionDurationValue = const Duration(milliseconds: 220),
    this.reverseTransitionDurationValue,
    this.alignment = Alignment.center,
    this.forwardCurve = Curves.easeOutCubic,
    this.reverseCurve = Curves.easeInCubic,
    this.logTag = 'BaseModalRoute',
    this.transitionBuilder,
  });
  // ... existing code ...
  Widget _defaultTransitionBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    final scaleTween = Tween<double>(begin: 0.85, end: 1);
    return FadeTransition(
      opacity: animation,
      child: ScaleTransition(
        alignment: alignment,
        scale: scaleTween.animate(animation),
        child: child,
      ),
    );
  }
}

为什么不用 showDialog 或手撸 Overlay

  • showDialog 不能自定义动效;
  • OverlayEntry 要自己算层级/销毁;
  • 每个弹窗都重写 ModalRoute,那还不如集中维护一个基类。

2. ModalTransitionPresets:常见动效信手拈来

  • 居中 fadeScale:用在弹窗、Toast。
  • slideFromBottom / slideFromTop / slideFromLeft / slideFromRight:抽成静态方法,任何 route 都可以直接调用。
  • 有新动效?往这里加即可,业务不用改。
class ModalTransitionPresets {
  const ModalTransitionPresets._();

  static ModalTransitionBuilder fadeScale({
    Alignment alignment = Alignment.center,
    double beginScale = 0.85,
  }) { /* ... */ }

  static ModalTransitionBuilder slideFromBottom({
    double beginOffsetY = 0.2,
    bool fade = true,
  }) { /* ... */ }

  static ModalTransitionBuilder slideFromTop({ /* ... */ })
  static ModalTransitionBuilder slideFromLeft({ /* ... */ })
  static ModalTransitionBuilder slideFromRight({ /* ... */ })
}

3. DirectionalPopoverRoute:锚点气泡的最终方案

  • 利用 GlobalKey 精确定位锚点位置,支持 anchorInsets 缩放矩形,popoverOffset 微调。
  • 默认动效是方向一致的淡入+滑入。
  • 提供 GlobalKey.showDirectionalPopover() 扩展,一行代码就能弹。
enum PopoverDirection { top, bottom, left, right }

class DirectionalPopoverRoute extends BaseModalRoute<void> {
  DirectionalPopoverRoute({
    required this.anchorKey,
    required this.direction,
    required this.child,
    this.gap = 0,
    this.margin = EdgeInsets.zero,
    this.maxWidth = 260,
    this.popoverOffset = Offset.zero,
    this.anchorInsets = EdgeInsets.zero,
    ModalBarrierConfig barrierConfig = const ModalBarrierConfig(
      color: Color(0x00000000), dismissible: true,
    ),
  }) : super(/* ... */);

  // ... existing code ...
}

/// 为使用 GlobalKey 的组件提供便捷的锚点弹窗能力。
extension DirectionalPopoverExtension on GlobalKey {
  Future<void> showDirectionalPopover({
    required BuildContext context,
    PopoverDirection direction = PopoverDirection.bottom,
    Widget? child,
    WidgetBuilder? builder,
    double gap = 0,
    EdgeInsets margin = EdgeInsets.zero,
    double maxWidth = 260,
    Offset popoverOffset = Offset.zero,
    EdgeInsets anchorInsets = EdgeInsets.zero,
    bool useRootNavigator = true,
    ModalBarrierConfig barrierConfig = const ModalBarrierConfig(
      color: Color(0x00000000), dismissible: true,
    ),
  }) {
    // ... existing code ...
  }
}

4. 实战演示:几种弹窗如何接入?

页面里,我们通过 BaseModalRoute 子类直接 push,动效与遮罩统一管理。例如:

Future<void> _showAnchorPopover() async {
  await _popoverAnchorKey.showDirectionalPopover(
    context: context,
    direction: PopoverDirection.bottom,
    popoverOffset: const Offset(0, -40),
    builder: (_) => Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            '筛选提示',
            style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
          ),
          SizedBox(height: 8),
          Text(
            'DirectionalPopoverRoute 基于 BaseModalRoute,可根据锚点朝向展示气泡弹窗,适合筛选按钮等场景。',
          ),
        ],
      ),
    ),
  );
}
  • 居中信息弹窗:InfoModalRoute,template + 可插入 builder。
  • 确认弹窗:ConfirmModalRoute,遮罩不可点击关闭,返回 bool。
  • 底部操作面板:BottomSheetModalRoute,自带 slideFromBottom 动效。
  • 锚点气泡:DirectionalPopoverRoute + extension。

5. 成果总结(及后续扩展方向)

  • 统一骨架:遮罩/动画/日志集中维护;跨项目也好迁移。
  • 动效预设:新增动效只是扩展 ModalTransitionPresets,不再重复造轮子。
  • 锚点精准:DirectionalPopoverRoute 专门做定位和偏移,不用再被 Overlay 坑到。
  • 内容自定义:每个 Route 都有 contentBuilder 插槽,模板“友好但不束缚”。
  • 后续扩展:想新增右下角弹窗、全屏模态,只需新建 Route 继承骨架 + 选择动效即可。

换句话说,我们构建的不是“某几个弹窗”,而是一整套可持续迭代的弹窗体系。这套体系让 Flutter 弹窗既能贴近原生体验,又能保持工程级的可维护性,真正实现“写一次,处处弹得对”。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容