Flutter 寫全域性彈框的心路歷程(dialog和overlay)

yk3372發表於2020-03-24

最近做了個小功能,要做一個全域性的彈窗,隨處都可以彈出,這個咋做呢? 說下從頭到尾的思路:

  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浮層了。

洋洋灑灑寫完了,上面是我做這個需求的整個分析及解決思路,大家可以參考下 ^_^。

最後,我們在做全域性彈框時,有這兩種方式可選,具體看需要哪種合適選哪個吧~~

相關文章