A more elegant fluent dialog solution

Time:2021-7-30

preface

The dialog provided by the system actually pushes a new page, which has many advantages, but there are also some problems that are difficult to solve

  • Buildcontext must be passed

    • The loading pop-up window is generally encapsulated in the network framework, and it is a headache to pass multiple context parameters; Use fish_ Redux is good. The effect layer can get the context directly. If you use bloc, you have to transfer the context to bloc or cube in the view layer…
  • Unable to penetrate the dark background, click the page behind the dialog

    • This is a real headache. I thought of many ways, but I couldn’t solve this problem on my own dialog
  • The system comes with a loading pop-up window written in dialog. In the case of network request and page Jump, there will be routing confusion

    • Scenario replay: the loading library is generally encapsulated in the network layer. After a page submits a form, it needs to jump to the page. After the submission operation is completed, the page Jump is performed. The loading is closed in an asynchronous callback (onerror or onsuccess). When the jump operation is performed, the pop-up window is not closed and will be closed after a delay of a little, because the pop-up page method is used, Will pop out the page you jump to
    • It is difficult to predict the pop-up scene on the stack. It is also a common method to predict whether the pop-up scene is more complex

These pain points above are fatalOf course, there are other solutions, such as:

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

Obviously, overlay has the best portability. At present, many third-party toast and dialog libraries use this scheme. They use some loading libraries. After looking at the source code and penetrating the background solution, it is very different from the expected effect. Some dialog libraries have toast display, but toast display cannot coexist with dialog (toast is a special information display, It should exist independently), so I need to rely on one more toast library

SmartDialog

Based on the above problems that are difficult to solve, we can only implement them ourselves. It took some time to implement a pub package. Basically, the pain points that should be solved have been solved, and there is no problem in practical business

effect

A more elegant fluent dialog solution

introduce

dependencies:
  flutter_smart_dialog: any
  • Note: the library has been migrated to empty security. Pay attention to version differentiation
#Last stable version before non empty security
dependencies:
  flutter_smart_dialog: ^1.3.1

use

  • Main entrance configuration

    • The main entry needs to be configured so that the dialog can be used without transmitting buildcontext
    • You only need to configure it at the builder parameter of 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) {
        return FlutterSmartDialog(child: child);
      },
    );
  }
}

///flutter 1.x
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Container(),
      builder: (BuildContext context, Widget child) {
        return FlutterSmartDialog(child: child);
      },
    );
  }
}

useFlutterSmartDialogJust wrap the child. Now you can use smartdialog happily

  • Use toast: because toast is special, toast is optimized here

    • Time: optional, duration type, 1500ms by default
    • Isdefaultdismisstype: the type of toast that disappears. The default value is true

      • True: the default disappearance type is similar to Android toast. Toast is displayed one by one
      • False: it is not the default vanishing type. After clicking for several times, the toast display of the former will be cancelled
    • Widget: you can customize toast
    • MSG: required message
    • Alignment: optional; controls the toast position
    • If you want to use the fancy toast effect, please use the showtoast method to customize it. It’s easy to fry chicken. You’re too lazy to write it yourself. Just copy my toast widget and change the properties
SmartDialog.showToast('test toast');
  • Using loading: loading has many setting properties. Please refer to the followingSmartdialog configuration parameter descriptionthat will do

    • MSG: optional, text message under loading animation (default: Loading…)
//open loading
SmartDialog.showLoading();

//delay off
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();
  • Custom dialog

    • Use the smartdialog. Show () method, which contains manyTempIs a parameter with suffix, and none belowTempThe parameters with suffix have the same function
    • Special propertiesisUseExtraWidget: whether to use additional cover floating layer, which can be separated from the main floating layer; It can be independent of loading and dialog. 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,
    ),
);
  • Smartdialog configuration parameter description

    • for fear ofinstanceToo many attributes are exposed, which makes it inconvenient to use. Many parameters are used hereinstanceMediumconfigAttribute management
    • The attributes set with config are global. These attributes are managed separately with config to facilitate modification and management of these attributes and to make the smartdialog class easier to maintain
parameter Function description
alignment Controls where custom controls are located on the screen < br / >Alignment.center: the custom control is located in the middle of the screen and is an animation. The default is fade and zoom. You can use isloading to select an animation < br / >Alignment.bottomCenter、Alignment.bottomLeft、Alignment.bottomRight: the custom control is located at the bottom of the screen. The animation defaults to displacement animation. From bottom to top, you can use animationduration to set the animation time < br / >Alignment.topCenter、Alignment.topLeft、Alignment.topRight: the custom control is located at the top of the screen. The animation defaults to displacement animation. From top to bottom, you can use animationduration to set the animation time < br / >Alignment.centerLeft: the custom control is located on the left side of the screen. The animation defaults to displacement animation. From left to right, you can use animationduration to set the animation time < br / >Alignment.centerRight: the custom control is located on the left side of the screen. The animation defaults to displacement animation. From right to left, you can use animationduration to set the animation time
isPenetrate Default: false; Whether to penetrate the mask background. Control after interactive mask, true: Click to penetrate the background, false: cannot penetrate; If the penetration mask is set to true, the background mask will automatically become transparent (required)
clickBgDismiss Default: true; Click the mask to close the dialog — true: click the mask to close the dialog, false: do not close it
maskColor Mask color
animationDuration Animation time
isUseAnimation Default: true; Use animation
isLoading Default: true; Whether to use loading animation; True: the content body uses fade animation false: the content body uses zoom animation, only for controls in the middle
isExist Default: false; Does the principal smartdialog (overlayentry) exist on the interface
isExistExtra Default: false; Does the additional smartdialog (overlayentry) exist on the interface
  • The config attribute is used, for example, chestnuts

    • Internally initialized related attributes; If you need to customize, you can initialize the properties you want 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;
  • Return to the event and close the pop-up solution

There is basically a problem with the dependency library using overlay. It is difficult to monitor the return events, which makes it difficult to close the pop-up layout for violating the return events. I have thought of many ways, but I can’t solve this problem in the dependency library. Here is an exampleBaseScaffold, use on each pageBaseScaffold, you can solve the problem of returning events and closing the dialog

typedef ScaffoldParamVoidCallback = void Function();

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);

  ///Properties of system scaffold
  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 whether to exit the page. Click twice quickly to exit the page
  final bool isTwiceBack;

  ///Can I return
  final bool isCanBack;

  ///Listen for return events
  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 return button
  Future<bool> dealWillPop() async {
    if (widget.onBack != null) {
      widget.onBack();
    }

    //Dealing with pop ups
    if (SmartDialog.instance.config.isExist) {
      SmartDialog.dismiss();
      return false;
    }

    //If you can't return, the following logic won't go
    if (!widget.isCanBack) {
      return false;
    }

    if (widget.isTwiceBack) {
      if (_lastPressedAt == null ||
          DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
        //If the interval between two clicks exceeds 1 second, the time will be re timed
        _lastPressedAt = DateTime.now();

        //Pop up prompt
        Smartdialog.showtoast ("click again to exit");
        return false;
      }
      return true;
    } else {
      return true;
    }
  }

  ///Some life cycles
  @override
  void initState() {
    super.initState();
  }

  @override
  void deactivate() {
    super.deactivate();
  }

  @override
  void dispose() {
    super.dispose();
  }
}

Solutions to several problems

Penetrating background

  • There are two solutions to penetrate the background, which are explained here

AbsorbPointer、IgnorePointer

At that time, when I wanted to penetrate the dark background and interact with the controls behind the background, I almost immediately thought of these two controls. Let’s learn about these two controls first

  • AbsorbPointer

    • Prevent subtrees from receiving pointer events,AbsorbPointerIt can respond to events and consume events
    • absorbingProperty (default true)

      • True: intercept events passed to child widgets false: do not intercept
AbsorbPointer(
    absorbing: true,
    child: Listener(
        onPointerDown: (event){
            print('+++++++++++++++++++++++++++++++++');
        },
    )
)
  • IgnorePointer

    • Prevent subtrees from receiving pointer events,IgnorePointerIt cannot respond to the event itself, and the control under it can receive the click event (parent control)
    • ignoringProperty (default true)

      • True: intercept events passed to child widgets false: do not intercept
IgnorePointer(
    ignoring: true,
    child: Listener(
        onPointerDown: (event){
            print('----------------------------------');
        },
    )
)

analysis

  • Let’s analyze it here. FirstAbsorbPointerThis control is not appropriate becauseAbsorbPointerItself will consume touch events, and events will beAbsorbPointerConsumption will lead to the page behind the background unable to obtain the touch event;IgnorePointerCannot consume touch events by itself, andIgnorePointerandAbsorbPointerBoth have the function of shielding sub widgets to obtain touch events. This seems reliable. Here you try, and you can interact with the page behind the background! However, there is a very serious problem
  • Because useIgnorePointerMask touch events of child controls, andIgnorePointerIt does not consume touch events, which will lead to the inability to obtain the click events of the background! In this way, clicking the background will not close the dialog pop-up window. You can only close the dialog manually; There is no way to get the touch event of the background through various attempts. This scheme of penetrating the background can only be abandoned

Listener、behavior

This scheme successfully achieves the desired penetration effect. Let’s learn about it herebehaviorSeveral properties of

  • Defertochild: only when a child is hit by a hit test, the target that succumbs to its child will receive events within its range
  • Opaque: opaque targets may be hit by hit tests, causing them to both receive events within their range and visually prevent targets behind them from receiving events
  • Translucent: a translucent target can receive events within its range or visually allow the target behind the target to also receive events

There’s a play! Obviously, translucent is promising. I tried it several times and successfully achieved the desired effect

Attention, there are several pits here. Mention it

  • Be sure to useListenerControl to use the behavior property. There is a problem when using the behavior property in gesturedetector. Generally speaking, it is children in the stack control. There are two controls in the stack control, which are divided into upper and lower layers. Here, gesturedetector sets the behavior property. The two gesturedetector controls are superimposed up and down, which will lead to that the lower gesturedetector cannot obtain the touch event. It is very strange; useListenerThis problem will not occur
  • Our background useContainerControl, I set it hereColors.transparent, the lower layer will not receive the touch event directly. If color is empty, the lower layer control can receive the touch event. Do not set color here

The following is a small example of verification

class TestLayoutPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //Lower layer
      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,
        ),
      ),
    );
  }
}

Toast and loading conflict

  • Theoretically, this problem will certainly exist, because generally, the overlay library will only use one overlayentry control, which will lead to the existence of only one floating window layout globally. Toast is essentially a global pop-up window, and loading is also a global pop-up window. Using one of them will lead to the disappearance of the other
  • Toast is obviously a message prompt that should be independent of other pop-up windows. The disass method of closing pop-up windows encapsulated in the network library will also close toast messages when it is inappropriate. This problem is encountered in actual development. We can only refer to one more toast third-party library to solve it. When planning this dialog Library, we thought we must solve this problem

    • An overlayentry is used internally to solve this problem, and relevant parameters are provided to control them respectively, which perfectly makes toast independent of other dialog pop-up windows
    • Adding one more overlayentry will make the use of internal logic and methods dramatically complex, and the maintenance will become unpredictable. Therefore, only one more overlayentry is provided; If you need more, you can copy this library, define it by yourself, and realize the relevant source code of the library. All of them strive to make people understand it. I believe you won’t feel obscure when using copy
  • Provided by fluttersmartdialogOverlayEntryandOverlayEntryExtraHighly customizable, related implementations, and internal implementations can be viewed
  • Fluttersmartdialog has been implemented internally, usingshow()In methodisUseExtraWidgetdistinguish

last

This library took some time to conceive and implement, which can be regarded as solving several big pain points

  • If everyone is rightReturn eventIf you have any good ideas, please let me know in the comments. Thank you!

Project address

Fluttersmartdialog some information

Series articles

State management