Flutter優(yōu)化Dialog使用

  • 概述

    Flutter中提供了對話框組件和彈出對話框的方法,但是這些方法都需要傳入一個BuildContext參數(shù),這使得我們必須要在構(gòu)建樹中傳入BuildContext才行,這限制了我們調(diào)起彈框的位置,所以我們需要一個可以在任何位置顯示和隱藏的彈框。

  • 原理

    Flutter的原生調(diào)用中,提供了諸如AlertDialog、SimpleDialog、Dialog等組件,并且提供了諸如showDialog、showModalBottomSheet(底部彈窗)等方法來顯示Dialog。

    組件部分沒什么好說的,我們可以定義自己的彈框樣式,主要是顯示和隱藏的原理。

    我們來想想一下可以實現(xiàn)一個對話框效果的方法。

    我們知道,F(xiàn)lutter的組件是通過構(gòu)建組件樹的方式顯示在界面上的,如果要實現(xiàn)一個對話框的效果,我們可能會首先想到通過Stack來實現(xiàn),把對話框放在Statck的最后一個,然后通過它的顯示和隱藏來達到效果。但是這樣一來,我們的對話框就和組件樹綁定在一起了,在實現(xiàn)層面上,它不會影響最終的呈現(xiàn),但是在規(guī)范上總覺得它不該屬于組件樹。不僅如此,這樣實現(xiàn),我們每個頁面需要對話框的時候都需要和頁面組件樹綁定,重復代碼太多,每個頁面都需要維護一個自己的彈框。所以,這種方式可以實現(xiàn)但是太不靈活。

    那原生是怎么做的?Flutter中使用路由的方式來實現(xiàn)對話框。

    showDialog方法中:

    return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(DialogRoute<T>(
      context: context,
      builder: builder,
      barrierColor: barrierColor,
      barrierDismissible: barrierDismissible,
      barrierLabel: barrierLabel,
      useSafeArea: useSafeArea,
      settings: routeSettings,
      themes: themes,
    ));
    

    可以看到,顯示就是一個push一個新頁面,那隱藏很明顯就是調(diào)用Navigator的pop方法。

    DialogRoute會間接繼承自PopupRoute,這個類中重寫了ModalRoute的兩個屬性值:

    //這個屬性可以設(shè)置遮蓋層透明度是否完全遮蓋住前一個頁面
    @override
    bool get opaque => false;
    //這個屬性設(shè)置當該route不可見時是否可以被內(nèi)存殺死
    @override
    bool get maintainState => true;
    

    這兩個屬性就保證了對話框的顯示效果以及和其他route一樣正常被系統(tǒng)處理。

    再往上一級,DialogRoute繼承了RawDialogRoute,RawDialogRoute繼承自PopupRoute,這個類中首先把DialogRoute構(gòu)造時傳入的參數(shù)和ModalRoute中的屬性對應(yīng)起來:

    //決定點擊遮蓋層是否可以pop該route
    @override
    bool get barrierDismissible => _barrierDismissible;
    final bool _barrierDismissible;
    //遮蓋層的顏色,這就是陰影區(qū)的顏色,可以通過給他設(shè)置透明度來達到半透明效果
    @override
    Color? get barrierColor => _barrierColor;
    final Color? _barrierColor;
    //route顯示隱藏的時長
    @override
    Duration get transitionDuration => _transitionDuration;
    final Duration _transitionDuration;
    

    然后重寫了ModalRoute的兩個方法:

    @override
    Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
      return Semantics(
        scopesRoute: true,
        explicitChildNodes: true,
        child: _pageBuilder(context, animation, secondaryAnimation),
      );
    }
    
    @override
    Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
      if (_transitionBuilder == null) {
        return FadeTransition(
          opacity: CurvedAnimation(
            parent: animation,
            curve: Curves.linear,
          ),
          child: child,
        );
      } // Some default transition
      return _transitionBuilder!(context, animation, secondaryAnimation, child);
    }
    

    我們知道,在Flutter的路由流程中,最終就是通過buildPage方法來獲取route中的組件樹的,這里通過暴露的_pageBuilder函數(shù)來設(shè)置組件。buildTransitions是用來構(gòu)建顯示隱藏的動畫轉(zhuǎn)換效果。

    通過上面的分析,我們發(fā)現(xiàn)這種實現(xiàn)要好了很多,至少我不需要在每個頁面的組件樹中插入Dialog組件了,但是還不夠優(yōu)雅,我們發(fā)現(xiàn)這里必須要傳入一個BuildContext,因為Navigator需要它,那么有什么方法解決這個問題呢?

  • get框架的實現(xiàn)

    首先我們要明白,Navigator中需要的BuildContext必須是當前顯示的route的context,因為我們要根據(jù)它才能知道我們的新route放在哪里。

    get框架中對dialog的使用也做了封裝,使用時不用再傳入BuildContext了,這就使得我們可以在非BuildContext持有類中操作Dialog,我們看一下他是怎么解決這個問題的。

    get通過Get.dialog方法來顯示一個Dialog,通過Get.back方法來關(guān)閉一個Dialog,dialog方法中:

    return generalDialog<T>(
      pageBuilder: (buildContext, animation, secondaryAnimation) {
        final pageChild = widget;
        Widget dialog = Builder(builder: (context) {
          return Theme(data: theme, child: pageChild);
        });
        if (useSafeArea) {
          dialog = SafeArea(child: dialog);
        }
        return dialog;
      },
      barrierDismissible: barrierDismissible,
      barrierLabel: MaterialLocalizations.of(context!).modalBarrierDismissLabel,
      barrierColor: barrierColor ?? Colors.black54,
      transitionDuration: transitionDuration ?? defaultDialogTransitionDuration,
      transitionBuilder: (context, animation, secondaryAnimation, child) {
        return FadeTransition(
          opacity: CurvedAnimation(
            parent: animation,
            curve: transitionCurve ?? defaultDialogTransitionCurve,
          ),
          child: child,
        );
      },
      navigatorKey: navigatorKey,
      routeSettings:
          routeSettings ?? RouteSettings(arguments: arguments, name: name),
    );
    

    generalDialog方法如下:

    Future<T?> generalDialog<T>({
      required RoutePageBuilder pageBuilder,
      bool barrierDismissible = false,
      String? barrierLabel,
      Color barrierColor = const Color(0x80000000),
      Duration transitionDuration = const Duration(milliseconds: 200),
      RouteTransitionsBuilder? transitionBuilder,
      GlobalKey<NavigatorState>? navigatorKey,
      RouteSettings? routeSettings,
    }) {
      assert(!barrierDismissible || barrierLabel != null);
      final nav = navigatorKey?.currentState ??
          Navigator.of(overlayContext!,
              rootNavigator:
                  true); //overlay context will always return the root navigator
      return nav.push<T>(
        GetDialogRoute<T>(
          pageBuilder: pageBuilder,
          barrierDismissible: barrierDismissible,
          barrierLabel: barrierLabel,
          barrierColor: barrierColor,
          transitionDuration: transitionDuration,
          transitionBuilder: transitionBuilder,
          settings: routeSettings,
        ),
      );
    }
    

    可見,get同樣是通過Navigator來實現(xiàn)的,我們來看它的BuildContext問題是怎么不需要傳遞的。

    我們發(fā)現(xiàn)它傳入了一個overlayContext:

    BuildContext? get overlayContext {
      BuildContext? overlay;
      key.currentState?.overlay?.context.visitChildElements((element) {
        overlay = element;
      });
      return overlay;
    }
    

    key是什么:

    GlobalKey<NavigatorState> get key => _getxController.key;
    //extension GetNavigation中
    static GetMaterialController _getxController = GetMaterialController();
    //GetMaterialController中
    var _key = GlobalKey<NavigatorState>(debugLabel: 'Key Created by default');
    

    而這個key是在第一次使用Navigator的時候在mount方法生成的,也可以理解成根NavigatorState的GlobalKey,所以通過它獲取的context也就是根NavigatorState的Element,然后通過visitChildElements方法循環(huán),最終會得到一個最上層的Overlay。什么是Overlay,它就是上面說的遮蓋層,每一個route都有自己的遮蓋層,我們的組件樹最終就是要呈現(xiàn)在它上面。

    所以原理就是通過根NavigatorState獲取最上層的(也就是當前顯示的)route的Element(也就是BuildContext),然后傳給Navigator來調(diào)起路由。

  • 總結(jié)

    現(xiàn)在我們知道了Flutter中Dialog是如何實現(xiàn)的,并且我們知道了怎么去優(yōu)化Dialog的使用讓它變得更優(yōu)雅。

    現(xiàn)在,我們可以結(jié)合get框架封裝一個工具類來使用它:

    class DialogUtil {
      static void show() {
        if (Get.isDialogOpen == true) {
          return;
        }
        Get.dialog(
          LoadingWidget(),
          barrierColor: Color.fromRGBO(0, 0, 0, 0.5),
          barrierDismissible: false,
        );
      }
    
      static void dismiss() {
        if (Get.isDialogOpen == true) {
          Get.back();
        }
      }
    }
    

    isDialogOpen在使用get進行路由跳轉(zhuǎn)時會記錄當前route是否是Dialog類型的,我們可以根據(jù)它來判斷當前dialog是否在展示,就是一個標識位,我們自己也不難實現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容