preface

The system’s built-in Dialog is essentially a Push of a new page, which has many benefits, but also some problems that are difficult to solve

  • Must pass BuildContext
    • Loading popups are generally encapsulated in the network framework, so it is troublesome to pass multiple context parameters. Fish_redux: The effect layer can fetch the context directly. If you use bloc, you have to pass the context to the Bloc or cubit in the View layer.
  • Unable to penetrate the dark background, click on the page after the dialog
    • This is a real headache, I tried many ways, but failed to solve the problem in the native dialog
  • The system has its own Loading pop-up written in Dialog. In the case of network requests and jump pages, there will be route chaos
    • Scenario replay: Loading library is generally packaged in the network layer. When a page is finished submitting a form, it will jump to the page, and then the page will jump to the page after the submission is completed. Loading is closed in the asynchronous callback (onError or onSuccess), and the popup window will be closed after a short delay. Because the use of pop page method, will jump to the page pop off
    • The above is a very common scenario, which is more difficult to predict when complex scenes are involved. The solution also includes: locating whether the top of the page stack is Loading popup, and selecting Pop, which is difficult to implement

The above pain points are fatal. Of course, there are some other solutions, such as:

  • Use Stack at the top of the page
  • Using the Overlay

Obviously, Overlay is the best portability. At present, many toast and Dialog tripartite libraries use this solution. Some loading libraries are used, and the source code is read. However, the Toast display does not co-exist with the Dialog (Toast is a special display of information and should stand on its own), so I need to rely on one more TOAST library

SmartDialog

Based on the above difficult to solve the problems, can only be realized by themselves, took some time to achieve a Pub package, the basic pain points have been solved, there is no problem for the actual business

The effect

  • Click on me to experience it

The introduction of

  • Pub:View the flutter_smart_dialog plug-in version
    • Since version 2.0, this library has been adapted for null security
dependencies:
  flutter_smart_dialog: any
Copy the code
  • Note: This library has been migrated to null security, note version differentiation
Dependencies: flutter_smart_dialog1.31.
Copy the code

use

  • Main entrance configuration
    • The main entry needs to be configured so that we don’t pass BuildContext to use Dialog
    • This only needs to be configured in the Builder parameter of the MaterialApp
void main() {
  runApp(MyApp());
}

///Flutter 2.0
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Container(),
      builder: (BuildContext context, Widget? child) {
        returnFlutterSmartDialog(child: child); }); }}///flutter 1.x
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Container(),
      builder: (BuildContext context, Widget child) {
        returnFlutterSmartDialog(child: child); }); }}Copy the code

Wrap the child with the FlutterSmartDialog, and now you’re happy to use the SmartDialog

  • Use Toast: Because Toast is special, some optimization has been made for Toast
    • Time: This parameter is optional. The default value is 1500ms
    • IsDefaultDismissType: The type of toast that disappears. The default is true
      • True: The default disappearing type, similar to Android toast, is displayed one by one
      • False: not the default disappearing type. If you click it several times, the toast will override the former toast display
    • Widget: You can customize toast
    • MSG: mandatory message
    • Alignment: Controls the toast position
    • If you want to use the fancy Toast effect, you can customize it by using the showToast method. If you don’t want to write it yourself, copy my ToastWidget and change its properties
SmartDialog.showToast('test toast');
Copy the code
  • Using Loading: Loading has a number of Settings, as shown belowSmartDialog Configuration parametersCan be
    • MSG: Optional, loading text message below animation (default: loading…)
//open loading
SmartDialog.showLoading();

//delay off
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();
Copy the code
  • Custom dialog
    • Just use the Smartdialog.show () method, which contains manyTempAre suffixed parameters, and the following are noneTempParameters with suffixes have the same functions
    • Special attributesisUseExtraWidget: Whether to use additional covering floating layer, which can be opened independently from the main floating layer; It can be opened independently with loading, dialog, etc. The built-in showToast enables this configuration and can coexist with loading
SmartDialog.show(
    alignmentTemp: Alignment.bottomCenter,
    clickBgDismissTemp: true,
    widget: Container(
      color: Colors.blue,
      height: 300,),);Copy the code
  • SmartDialog Configuration parameters
    • In order to avoidinstanceToo many attributes are exposed inside, resulting in inconvenience, so many parameters are used hereinstanceIn theconfigProperty management
    • Properties set using config are global and are managed separately using Config to make it easier to modify and manage these properties and to make the SmartDialog class more maintainable
parameter Functional specifications
alignment Controls where custom controls are located on the screen

Alignment.center: The custom control is located in the middle of the screen and is animated. The default is fade and zoom. You can use isLoading to select animations

BottomCenter, bottom. bottomLeft, and bottom. right: custom controls at the bottom of the screen, animation defaults to displacement animation, bottom-up, animationDuration can be set using animationDuration

Alignment. TopCenter, Alignment. TopLeft, Alignment. TopRight: custom controls at the top of the screen, animation defaults to displacement animation, top down, animationDuration can be set using animationDuration

Alignment.centerLeft: custom control on the left side of the screen, animation defaults to displacement animation from left to right, animationDuration can be set using animationDuration

Alignment.centerRight: custom control on the left side of the screen, animation defaults to displacement animation from right to left, animationDuration can be set using animationDuration
isPenetrate Default: false; Control after interactive mask, true: click can penetrate the background, false: can not penetrate; Mask penetration set to True, background mask automatically becomes transparent (required)
clickBgDismiss Default: true; Click mask to close dialog– true: click mask to close dialog, false: do not close dialog
maskColor Mask color
animationDuration Animation time
isUseAnimation Default: true; Whether to use animation
isLoading Default: true; Whether to use Loading animation; True: Use fade animation for the content body false: Use zoom animation for the content body, only for the controls in the middle
isExist Default: false; Whether the main SmartDialog (OverlayEntry) exists on the interface
isExistExtra Default: false; Whether extra SmartDialog (OverlayEntry) exists on the interface
  • The Config property is used, for example
    • Internal initialization of related attributes; If you need customization, you can initialize the desired properties at the main entrance
SmartDialog.instance.config .. clickBgDismiss =true
    ..isLoading = true
    ..isUseAnimation = true
    ..animationDuration = Duration(milliseconds: 270)
    ..isPenetrate = false
    ..maskColor = Colors.black.withOpacity(0.1)
    ..alignment = Alignment.center;
Copy the code
  • Returns the event to close the popover solution

There is usually a problem with dependency libraries that use Overlay. It is difficult to listen to return events, so it is difficult to close the popover layout for offending return events. We have tried many ways, but we cannot solve this problem in the dependency library. This solves the return event closing Dialog problem

  • Flutter 2.0
typedef ScaffoldParamVoidCallback = void Function(a);class BaseScaffold extends StatefulWidget {
  const BaseScaffold({
    Key? key,
    this.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
    this.persistentFooterButtons,
    this.drawer,
    this.endDrawer,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomInset,
    this.primary = true.this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false.this.extendBodyBehindAppBar = false.this.drawerScrimColor,
    this.drawerEdgeDragWidth,
    this.drawerEnableOpenDragGesture = true.this.endDrawerEnableOpenDragGesture = true.this.isTwiceBack = false.this.isCanBack = true.this.onBack,
  })  : assert(primary ! =null),
        assert(extendBody ! =null),
        assert(extendBodyBehindAppBar ! =null),
        assert(drawerDragStartBehavior ! =null),
        super(key: key);

  ///The properties of the Scaffold in the system
  final bool extendBody;
  final bool extendBodyBehindAppBar;
  final PreferredSizeWidget? appBar;
  final Widget? body;
  final Widget? floatingActionButton;
  final FloatingActionButtonLocation? floatingActionButtonLocation;
  final FloatingActionButtonAnimator? floatingActionButtonAnimator;
  final List<Widget>? persistentFooterButtons;
  final Widget? drawer;
  final Widget? endDrawer;
  final Color? drawerScrimColor;
  final Color? backgroundColor;
  final Widget? bottomNavigationBar;
  final Widget? bottomSheet;
  final bool? resizeToAvoidBottomInset;
  final bool primary;
  final DragStartBehavior drawerDragStartBehavior;
  final double? drawerEdgeDragWidth;
  final bool drawerEnableOpenDragGesture;
  final bool endDrawerEnableOpenDragGesture;

  ///Added attributes
  ///Click the "back" button to prompt you to exit the page, and click it twice quickly to exit the page
  final bool isTwiceBack;

  ///Can I return?
  final bool isCanBack;

  ///Listen for return event
  final ScaffoldParamVoidCallback? onBack;

  @override
  _BaseScaffoldState createState() => _BaseScaffoldState();
}

class _BaseScaffoldState extends State<BaseScaffold> {
  DateTime? _lastPressedAt; // Last click time

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      child: Scaffold(
        appBar: widget.appBar,
        body: widget.body,
        floatingActionButton: widget.floatingActionButton,
        floatingActionButtonLocation: widget.floatingActionButtonLocation,
        floatingActionButtonAnimator: widget.floatingActionButtonAnimator,
        persistentFooterButtons: widget.persistentFooterButtons,
        drawer: widget.drawer,
        endDrawer: widget.endDrawer,
        bottomNavigationBar: widget.bottomNavigationBar,
        bottomSheet: widget.bottomSheet,
        backgroundColor: widget.backgroundColor,
        resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
        primary: widget.primary,
        drawerDragStartBehavior: widget.drawerDragStartBehavior,
        extendBody: widget.extendBody,
        extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
        drawerScrimColor: widget.drawerScrimColor,
        drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
        drawerEnableOpenDragGesture: widget.drawerEnableOpenDragGesture,
        endDrawerEnableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
      ),
      onWillPop: dealWillPop,
    );
  }

  ///Control back button
  Future<bool> dealWillPop() async {
    if(widget.onBack ! =null) { widget.onBack! (a); }// Handle popovers
    if (SmartDialog.instance.config.isExist) {
      SmartDialog.dismiss();
      return false;
    }

    // If it does not return, the logic behind it will not work
    if(! widget.isCanBack) {return false;
    }

    if (widget.isTwiceBack) {
      if (_lastPressedAt == null ||
          DateTime.now().difference(_lastPressedAt!) > Duration(seconds: 1)) {
        // Reset the timer if the interval between two clicks exceeds 1 second
        _lastPressedAt = DateTime.now();

        // Popup prompt
        SmartDialog.showToast("Click exit again.");
        return false;
      }
      return true;
    } else {
      return true; }}}Copy the code
  • Flutter 1.x
typedef ScaffoldParamVoidCallback = void Function(a);class BaseScaffold extends StatefulWidget {
  const BaseScaffold({
    Key key,
    this.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
    this.persistentFooterButtons,
    this.drawer,
    this.endDrawer,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomInset,
    this.primary = true.this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false.this.extendBodyBehindAppBar = false.this.drawerScrimColor,
    this.drawerEdgeDragWidth,
    this.drawerEnableOpenDragGesture = true.this.endDrawerEnableOpenDragGesture = true.this.isTwiceBack = false.this.isCanBack = true.this.onBack,
  })  : assert(primary ! =null),
        assert(extendBody ! =null),
        assert(extendBodyBehindAppBar ! =null),
        assert(drawerDragStartBehavior ! =null),
        super(key: key);

  ///The properties of the Scaffold in the system
  final bool extendBody;
  final bool extendBodyBehindAppBar;
  final PreferredSizeWidget appBar;
  final Widget body;
  final Widget floatingActionButton;
  final FloatingActionButtonLocation floatingActionButtonLocation;
  final FloatingActionButtonAnimator floatingActionButtonAnimator;
  final List<Widget> persistentFooterButtons;
  final Widget drawer;
  final Widget endDrawer;
  final Color drawerScrimColor;
  final Color backgroundColor;
  final Widget bottomNavigationBar;
  final Widget bottomSheet;
  final bool resizeToAvoidBottomInset;
  final bool primary;
  final DragStartBehavior drawerDragStartBehavior;
  final double drawerEdgeDragWidth;
  final bool drawerEnableOpenDragGesture;
  final bool endDrawerEnableOpenDragGesture;

  ///Added attributes
  ///Click the "back" button to prompt you to exit the page, and click it twice quickly to exit the page
  final bool isTwiceBack;

  ///Can I return?
  final bool isCanBack;

  ///Listen for return event
  final ScaffoldParamVoidCallback onBack;

  @override
  _BaseScaffoldState createState() => _BaseScaffoldState();
}

class _BaseScaffoldState extends State<BaseScaffold> {
  DateTime _lastPressedAt; // Last click time

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      child: Scaffold(
        appBar: widget.appBar,
        body: widget.body,
        floatingActionButton: widget.floatingActionButton,
        floatingActionButtonLocation: widget.floatingActionButtonLocation,
        floatingActionButtonAnimator: widget.floatingActionButtonAnimator,
        persistentFooterButtons: widget.persistentFooterButtons,
        drawer: widget.drawer,
        endDrawer: widget.endDrawer,
        bottomNavigationBar: widget.bottomNavigationBar,
        bottomSheet: widget.bottomSheet,
        backgroundColor: widget.backgroundColor,
        resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
        primary: widget.primary,
        drawerDragStartBehavior: widget.drawerDragStartBehavior,
        extendBody: widget.extendBody,
        extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
        drawerScrimColor: widget.drawerScrimColor,
        drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
        drawerEnableOpenDragGesture: widget.drawerEnableOpenDragGesture,
        endDrawerEnableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
      ),
      onWillPop: dealWillPop,
    );
  }

  ///Control back button
  Future<bool> dealWillPop() async {
    if(widget.onBack ! =null) {
      widget.onBack();
    }

    // Handle popovers
    if (SmartDialog.instance.config.isExist) {
      SmartDialog.dismiss();
      return false;
    }

    // If it does not return, the logic behind it will not work
    if(! widget.isCanBack) {return false;
    }

    if (widget.isTwiceBack) {
      if (_lastPressedAt == null ||
          DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
        // Reset the timer if the interval between two clicks exceeds 1 second
        _lastPressedAt = DateTime.now();

        // Popup prompt
        SmartDialog.showToast("Click exit again.");
        return false;
      }
      return true;
    } else {
      return true; }}}Copy the code

Several problem solutions

Through the background

  • There are two solutions to penetrating the background, both of which are explained here

AbsorbPointer, IgnorePointer

When I wanted to solve the problem of penetrating dark background and interacting with the controls behind the background, I almost immediately thought of these two controls. Let’s get to know these two controls first

  • AbsorbPointer
    • Prevents the subtree from receiving pointer events, and the AbsorbPointer itself responds to the event and consumes the event

    • Magnetism attribute (default true)

      • True: intercepts events passed to child widgets False: does not intercept events
AbsorbPointer(
    absorbing: true,
    child: Listener(
        onPointerDown: (event){
            print("+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '); },))Copy the code
  • IgnorePointer
    • Prevents subtrees from receiving pointer events,IgnorePointerUnable to respond to the event itself, controls under it can receive click events (parent control)
    • ignoringProperty (default true)
      • True: intercepts events passed to child widgets False: does not intercept events
IgnorePointer(
    ignoring: true,
    child: Listener(
        onPointerDown: (event){
            print('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --'); },))Copy the code

Analysis of the

  • So let’s do the analysis. First of allAbsorbPointerThis control is not appropriate becauseAbsorbPointerItself will consume touch events, events areAbsorbPointerConsume, it will cause the page behind the background can not get the touch event;IgnorePointerItself unable to consume touch events, again due toIgnorePointerandAbsorbPointerEach of these widgets can be used to block touch events from being accessed by the child Widget. But there is a problem
  • Because the use ofIgnorePointerMask touch events for child controls, whileIgnorePointerIt does not consume touch events itself, resulting in the inability to get background click events! Clicking on the background will not close the Dialog popover. Various attempts, it is impossible to obtain the background of the touch event, such a penetrating background scheme can only be abandoned

The Listener, behaviors,

This scheme successfully achieves the desired penetration effect. Let’s take a look at several properties of behavior

  • DeferToChild: Only when a child is hit by a hit test will the target that succumbs to its child receive events in its range
  • Opaque: Targets may be hit by a hit test, causing them to both receive events within their range and visually prevent targets located behind them from receiving events as well
  • 3. A translucent target always receives an event in its scope or visually allows the target immediately behind the target to receive the event as well

The plus! 3. Clearly aware of being hopeful, always tries several times and succeeds in achieving the desired result

Notice, there’s a couple of potholes here. Mention them

  • Make sure to use the Listener control to use the behavior property. There is a problem with using the behavior property in the GestureDetector. Generally speaking: Here, the GestureDetector sets the behavior property. The two GestureDetector controls are stacked up and down, so that the lower GestureDetector cannot obtain the touch event. It’s strange; Using a Listener does not cause this problem

  • Transparent is used in the background of the Container control. Transparent will directly prevent the lower layer from receiving touch events. Color is empty so that the lower layer can receive touch events

Here is a small validation example written

class TestLayoutPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      / / below
      Listener(
        onPointerDown: (event) {
          print('Lower blue area ++++++++');
        },
        child: Container(
          height: 300,
          width: 300,
          color: Colors.blue,
        ),
      ),

      // Upper level event penetration
      Listener(
        behavior: HitTestBehavior.translucent,
        onPointerDown: (event) {
          print('Upper area ---------');
        },
        child: Container(
          height: 200,
          width: 200,),),]); } Widget _buildBg({List<Widget> children}) {
    return Scaffold(
      appBar: AppBar(title: Text('Test layout')), body: Center( child: Stack( alignment: Alignment.center, children: children, ), ), ); }}Copy the code

Toast conflicts with Loading

  • This problem will definitely exist in theory, because generally, Overlay libraries only use one OverlayEntry control, which will result in only one floating window layout globally. Toast is essentially a global popover, and Loading is also a global popover. Using one popover will cause the other to disappear

  • Toast is obviously a message prompt that should be independent of other popover. The dismiss method of closing popover enclosed in the network library will also close the Toast message when it is not suitable. This problem is encountered in actual development, and we have to refer to one more Toast three-party library to solve it. I thought I had to solve this problem

    • Here, an OverlayEntry is used internally to solve the problem, and relevant parameters are provided for separate control, making Toast perfectly independent of other Dialog pop-ups
    • Adding one more OverlayEntry will make the internal logic and method use extremely complicated, and the maintenance will become unpredictable. Therefore, only one more OverlayEntry will be provided. If you need more, you can copy this library, define yourself, to achieve the relevant source code of the library, and strive to make people understand, I believe that you will not feel obscure when using copy
  • FlutterSmartDialog provides OverlayEntry and OverlayEntryExtra that can be highly customizable. For related implementations, see the internal implementation

  • FlutterSmartDialog is already implemented internally, distinguished by isUseExtraWidget in the show() method

The last

The library took some time to conceptualize and implement, addressing several major pain points

  • If you think aboutReturn to the eventHave any good processing idea, trouble inform in the comment, thank!

The project address

FlutterSmartDialog Some information

  • Making: flutter_smart_dialog

  • Pub: flutter_smart_dialog

  • Use the effects to experience: Click to experience

series

State management

  • Wall cracks recommended for small projects: Use Flutter GetX — simple charm!

  • Large project recommendation: Fish_redux use details — after reading will use!

  • Flutter_bloc — You’re still building the flutter_bloc!