Flutter 写全局弹框的心路历程(dialog和overlay)

9,510 阅读4分钟

最近做了个小功能,要做一个全局的弹窗,随处都可以弹出,这个咋做呢? 说下从头到尾的思路:

  1. 之前看过文章写过如何不使用context进行路由跳转,正常情况我们都是这么写: Navigator.of(context).pushNamed('new_page');

都是需要传一个context才可以的。 但有时我们可能需要在没法传context的时候跳转咋写呢?

我们可以这样做:

// 先新建一个navigatorKey
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

//然后,找到我们的MaterialApp
MaterialApp(
    navigatorKey: navigatorKey,//加上此配置
    title: 'Flutter Demo',
    theme: ThemeData.light(),
    home: HomePage(),
)

然后我们页面跳转就可以这样写了:

navigatorKey.currentState.pushNamed('new_page');

好了,我们实现无context跳转了。

回归正题,既然有了这个state,我们能否用里面的context呢? 然后我就兴奋的去尝试一下:

showDialog(
    context: navigatorKey.currentState.context,
    builder: (context) => AlertDialog(
    content: Text('content'),
   ),
)

结果,很是失望!竟然报错了:

Log: The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.

原来这个context只能用于路由处理,为什么呢?

从调用栈看了下,

showDialog->showGeneralDialog->Navigator.of(context, rootNavigator: useRootNavigator).pushxxx

最后看到了这里:

static NavigatorState of(
    BuildContext context, {
    bool rootNavigator = false,
    bool nullOk = false,
  }) {
    final NavigatorState navigator = rootNavigator
        ? context.findRootAncestorStateOfType<NavigatorState>()
        : context.findAncestorStateOfType<NavigatorState>();
    assert(() {
      if (navigator == null && !nullOk) {
        throw FlutterError(
          'Navigator operation requested with a context that does not include a Navigator.\n'
          'The context used to push or pop routes from the Navigator must be that of a '
          'widget that is a descendant of a Navigator widget.'
        );
      }
      return true;
    }());
    return navigator;
  }

我们看到了错误那个串字符,然后我们拿出核心的:

findRootAncestorStateOfType<NavigatorState>findAncestorStateOfType<NavigatorState>

用过Inheritedxxx的应该比较熟悉这个,是用来从叶子节点,通过context向上查找根组件对象的,这里也就是寻找NavigatorState对象。

但从对应的实现来看,这两种方式查找初始值都是Element ancestor = _parent;,也就是从parent开始找,而当前的context就是这个state,他的parent自然是再也找不到了。因此这种简单的方式是不行了。

但也不要失望,至少我们从这次错误中,我们还能从showGeneralDialog发现这个:

Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(xxx

哦,原来对话框也是一种路由页面,所以我们可以仿照源码改出一份来,这里我就不说了,思路就是push一个自己写的dialogrouter,push进来就好。


  1. 接下来再介绍第二种方式,浮层:Overlay的方式: Overlay的日常使用,比如popupwindow之类的,使用方法:
final overlay = Overlay.of(context);// 获取一个overlay

// 创建一个OverlayEntry
OverlayEntry entry = OverlayEntry(
  builder: (context) {
    return Material(
      type: MaterialType.transparency,
      child: Stack(
        children: <Widget>[
          Positioned(
            top: 200,
            left: 200,
            child: GestureDetector(
              onTap: () {
                entry.remove();
                entry = null;
              },
              child: Container(
                color: Colors.redAccent
                child: Text('hahaha'),
                width: 100,
                height: 100,
              ),
            ),
          ),
        ],
      ),
    );
  },
);

// 添加进来即可显示
overlay.insert(entry);

好了,看完这个小demo,我们发现,弹浮窗也需要context。 我们先试一下:final overlay = Overlay.of(navigatorKey.currentState.context);

运行后,发现overlay是空,也是不能直接用,为什么呢?

我们看一下Overlay这个Widget在哪初始化的,经过搜索,发现是在Navigator里面build初始化的,也就是说,overlay是Navigator的child。

那经过上面dialog的经验,这里一样的问题,也是找不到的,因为也是从navigator的parent开始的,肯定找不到。

那Overlay该怎么用呢?这个又不是路由,不能push。

说实话,当时我没有思路,我就各种搜啊搜~~

咦,发现了个给力的库,顺便给大家推荐下:bot_toast

支持各种弹框,toast,对话框,通知,跨页面啥的都支持。

用完后,我学习了下他的实现方式,用的就是Overlay的方式,看到他的获取Overlay的方式。

核心就是:使用了NavigatorState里面的overlay对象,我很惊讶,这个navigator里面还有这么个方法?

看了下源码,果然:OverlayState get overlay => _overlayKey.currentState;

那就好说了,我们可以用刚刚那个navigatorKey来获取overlay了,获取方式:navigatorKey.currentState.overlay, 好了,有了这个overlay,我们就可以随时add浮层了。

洋洋洒洒写完了,上面是我做这个需求的整个分析及解决思路,大家可以参考下 ^_^。

最后,我们在做全局弹框时,有这两种方式可选,具体看需要哪种合适选哪个吧~~

本人一直在做Flutter相关开发,为了更好地互相交流相关问题,刚刚搞了个群,欢迎大家加入呀,一起学习交流Flutter知识。 image.png