Flutter (55): Detailed explanation of dialog boxes

Time: Column:Mobile & Frontend views:282

This section will provide a detailed overview of how to use dialogs in Flutter, including their implementation principles, style customization, and state management.

55.1 Using Dialogs

Dialogs are essentially UI layouts that typically include a title, content, and some action buttons. The Material library provides several pre-built dialog components to quickly construct a complete dialog.

1. AlertDialog

Here, we will mainly introduce the AlertDialog component from the Material library, which is defined as follows:

const AlertDialog({
  Key? key,
  this.title, // Dialog title component
  this.titlePadding, // Title padding
  this.titleTextStyle, // Title text style
  this.content, // Dialog content component
  this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), // Content padding
  this.contentTextStyle, // Content text style
  this.actions, // Dialog action button group
  this.backgroundColor, // Dialog background color
  this.elevation, // Dialog shadow
  this.semanticLabel, // Dialog semantic label (used for screen readers)
  this.shape, // Dialog shape
})

The parameters are quite straightforward, so we won't elaborate further. Let's look at an example where we need to show a confirmation dialog when deleting a file, as shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

The style code for this dialog is as follows:

AlertDialog(
  title: Text("Tip"),
  content: Text("Are you sure you want to delete the current file?"),
  actions: <Widget>[
    TextButton(
      child: Text("Cancel"),
      onPressed: () => Navigator.of(context).pop(), // Close the dialog
    ),
    TextButton(
      child: Text("Delete"),
      onPressed: () {
        // ... Execute delete operation
        Navigator.of(context).pop(true); // Close the dialog
      },
    ),
  ],
);

The implementation code is quite simple, so we won't go into detail. The only thing to note is that we close the dialog using the Navigator.of(context).pop(...) method, which is consistent with how routes return, and we can return a result data. Now that we have constructed the dialog, how do we show it? Also, how should we receive the data returned from the dialog? The answers to these questions lie in the showDialog() method.

showDialog() is a method provided by the Material component library for popping up a Material-style dialog, with the following signature:

Future<T?> showDialog<T>({
  required BuildContext context,
  required WidgetBuilder builder, // Builder for dialog UI
  bool barrierDismissible = true, // Whether to close the dialog when tapping the barrier (overlay)
})

This method has only two parameters, as described in the comments. It returns a Future, which is used to receive the return value of the dialog: if we close the dialog by tapping the overlay, the value of the Future will be null; otherwise, it will be the result value returned by Navigator.of(context).pop(result). Below is the complete example:

// After clicking this button, the dialog will pop up
ElevatedButton(
  child: Text("Dialog 1"),
  onPressed: () async {
    // Pop up the dialog and wait for it to close
    bool? delete = await showDeleteConfirmDialog1();
    if (delete == null) {
      print("Delete canceled");
    } else {
      print("Delete confirmed");
      // ... Delete the file
    }
  },
),

// Pop up the dialog
Future<bool?> showDeleteConfirmDialog1() {
  return showDialog<bool>(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text("Tip"),
        content: Text("Are you sure you want to delete the current file?"),
        actions: <Widget>[
          TextButton(
            child: Text("Cancel"),
            onPressed: () => Navigator.of(context).pop(), // Close the dialog
          ),
          TextButton(
            child: Text("Delete"),
            onPressed: () {
              // Close the dialog and return true
              Navigator.of(context).pop(true);
            },
          ),
        ],
      );
    },
  );
}

After running the example, if we click the "Cancel" button on the dialog or the overlay, the console will output "Delete canceled." If we click the "Delete" button, the console will output "Delete confirmed."

Note: If the content of the AlertDialog is too long, it will overflow, which is often not desired. Therefore, if the dialog content is long, we can wrap the content in a SingleChildScrollView.

2. SimpleDialog

SimpleDialog is another dialog provided by the Material component library that displays a list for selection scenarios. Below is an example for selecting the app's language, with the running result shown in Figure .

Flutter (55): Detailed explanation of dialog boxes

The implementation code is as follows:

Future<void> changeLanguage() async {
  int? i = await showDialog<int>(
      context: context,
      builder: (BuildContext context) {
        return SimpleDialog(
          title: const Text('Please select a language'),
          children: <Widget>[
            SimpleDialogOption(
              onPressed: () {
                // Return 1
                Navigator.pop(context, 1);
              },
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 6),
                child: const Text('Simplified Chinese'),
              ),
            ),
            SimpleDialogOption(
              onPressed: () {
                // Return 2
                Navigator.pop(context, 2);
              },
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 6),
                child: const Text('American English'),
              ),
            ),
          ],
        );
      });

  if (i != null) {
    print("Selected: ${i == 1 ? "Simplified Chinese" : "American English"}");
  }
}

In the example, we used the SimpleDialogOption component to wrap the list item components. It acts like a TextButton, except the button text is left-aligned and has smaller padding. After running the example, once the user selects a language, the console will print the selection.

3. Dialog

In fact, both AlertDialog and SimpleDialog utilize the Dialog class. Since AlertDialog and SimpleDialog use IntrinsicWidth to attempt to adjust their sizes based on the actual dimensions of child components, this causes their child components to not be able to use lazily-loaded models (such as ListView, GridView, CustomScrollView, etc.). For example, the following code will result in an error when run:

AlertDialog(
  content: ListView(
    children: ... // omitted
  ),
);

What should we do if we need to nest a ListView? In this case, we can directly use the Dialog class, like this:

Dialog(
  child: ListView(
    children: ... // omitted
  ),
);

Now, let's look at an example that pops up a dialog with 30 list items, as shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

The implementation code is as follows:

Future<void> showListDialog() async {
  int? index = await showDialog<int>(
    context: context,
    builder: (BuildContext context) {
      var child = Column(
        children: <Widget>[
          ListTile(title: Text("Please select")),
          Expanded(
              child: ListView.builder(
            itemCount: 30,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                title: Text("$index"),
                onTap: () => Navigator.of(context).pop(index),
              );
            },
          )),
        ],
      );
      // Using AlertDialog will cause an error
      // return AlertDialog(content: child);
      return Dialog(child: child);
    },
  );
  if (index != null) {
    print("Clicked: $index");
  }
}

Now that we have covered AlertDialog, SimpleDialog, and Dialog, it is worth noting that in the examples above, when calling showDialog, we constructed one of these three dialog components in the builder. Some readers might instinctively think that the builder can only return one of these three, but this is not mandatory! For instance, we can completely replace Dialog with the following code:

// return Dialog(child: child) 
return UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: ConstrainedBox(
    constraints: BoxConstraints(maxWidth: 280),
    child: Material(
      child: child,
      type: MaterialType.card,
    ),
  ),
);

Running the above code will achieve the same effect. In summary, AlertDialog, SimpleDialog, and Dialog are three types of dialogs provided by the Material component library designed to help developers quickly build dialogs that conform to Material design specifications. However, readers can fully customize the dialog styles, allowing for various styles of dialogs, thus providing ease of use while also being highly extensible.


55.2 Dialog Opening Animation and Overlay

We can divide the dialog into two parts: internal style and external style. The internal style refers to the specific content displayed in the dialog, which we have already introduced above. The external style includes the overlay style of the dialog, opening animations, etc. This section mainly discusses how to customize these external styles.

We will detail animation-related content in Chapter 9 of this book. For now, readers can get a basic understanding (there’s no need to delve too deeply into it) and revisit it after learning about animations.

We have already introduced the showDialog method, which is provided by the Material component library for opening a Material-style dialog. But how can we open a standard style dialog (non-Material)? Flutter provides a showGeneralDialog method with the following signature:

Future<T?> showGeneralDialog<T>({
  required BuildContext context,
  required RoutePageBuilder pageBuilder, // Builds the internal UI of the dialog
  bool barrierDismissible = false, // Whether to close the dialog when tapping the overlay
  String? barrierLabel, // Semantic label (for screen readers)
  Color barrierColor = const Color(0x80000000), // Overlay color
  Duration transitionDuration = const Duration(milliseconds: 200), // Duration of the dialog open/close animation
  RouteTransitionsBuilder? transitionBuilder, // Animation for opening/closing the dialog
  ...
})

In fact, the showDialog method is just a wrapper around showGeneralDialog, customizing the overlay color and animation for Material-style dialogs. The open/close animation for Material-style dialogs is a fade animation. If we want to use a scale animation, we can customize it through the transitionBuilder. Below is a method showCustomDialog that we can encapsulate, which customizes the dialog animation to a scale animation and sets the overlay color to Colors.black87:

Future<T?> showCustomDialog<T>({
  required BuildContext context,
  bool barrierDismissible = true,
  required WidgetBuilder builder,
  ThemeData? theme,
}) {
  final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
  return showGeneralDialog(
    context: context,
    pageBuilder: (BuildContext buildContext, Animation<double> animation,
        Animation<double> secondaryAnimation) {
      final Widget pageChild = Builder(builder: builder);
      return SafeArea(
        child: Builder(builder: (BuildContext context) {
          return theme != null
              ? Theme(data: theme, child: pageChild)
              : pageChild;
        }),
      );
    },
    barrierDismissible: barrierDismissible,
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
    barrierColor: Colors.black87, // Custom overlay color
    transitionDuration: const Duration(milliseconds: 150),
    transitionBuilder: _buildMaterialDialogTransitions,
  );
}

Widget _buildMaterialDialogTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child) {
  // Use scale animation
  return ScaleTransition(
    scale: CurvedAnimation(
      parent: animation,
      curve: Curves.easeOut,
    ),
    child: child,
  );
}

Now, we will use showCustomDialog to open a file deletion confirmation dialog, as shown in the following code:

... // omitted irrelevant code
showCustomDialog<bool>(
  context: context,
  builder: (context) {
    return AlertDialog(
      title: Text("Tip"),
      content: Text("Are you sure you want to delete the current file?"),
      actions: <Widget>[
        TextButton(
          child: Text("Cancel"),
          onPressed: () => Navigator.of(context).pop(),
        ),
        TextButton(
          child: Text("Delete"),
          onPressed: () {
            // Execute delete operation
            Navigator.of(context).pop(true);
          },
        ),
      ],
    );
  },
);

The running effect is shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

It can be observed that the overlay color is darker compared to the dialog opened using the showDialog method. Additionally, the dialog open/close animation has changed to a scale animation. Readers can run the example themselves to see the effect.


55.3 Implementation Principle of Dialogs

Let's take the showGeneralDialog method as an example to examine its specific implementation:

Future<T?> showGeneralDialog<T extends Object?>({
  required BuildContext context,
  required RoutePageBuilder pageBuilder,
  bool barrierDismissible = false,
  String? barrierLabel,
  Color barrierColor = const Color(0x80000000),
  Duration transitionDuration = const Duration(milliseconds: 200),
  RouteTransitionsBuilder? transitionBuilder,
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
}) {
  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
    settings: routeSettings,
  ));
}

The implementation is straightforward; it directly calls the push method of the Navigator to open a new dialog route, RawDialogRoute, and then returns the value from push. It is evident that dialogs are implemented in the form of routes, which is why we can use the Navigator.pop method to exit the dialog. As for customizing the dialog's style, this is done within RawDialogRoute, and readers can refer to it themselves for more details.


55.4 Dialog State Management

When a user chooses to delete a file, we will ask whether to delete that file; when a user selects a folder, we should confirm whether to delete its subfolders. To avoid a second popup confirmation when the user selects a folder, we add a checkbox at the bottom of the confirmation dialog labeled "Also delete subdirectories?" as shown in Figure .

Flutter (55): Detailed explanation of dialog boxes

Now, a question arises: how do we manage the selection state of the checkbox? Conventionally, we manage the selection state within the State of the route page, and we might write code like this:

class _DialogRouteState extends State<DialogRoute> {
  bool withTree = false; // Checkbox selection state

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ElevatedButton(
          child: Text("Dialog 2"),
          onPressed: () async {
            bool? delete = await showDeleteConfirmDialog2();
            if (delete == null) {
              print("Cancel deletion");
            } else {
              print("Also delete subdirectories: $delete");
            }
          },
        ),
      ],
    );
  }

  Future<bool?> showDeleteConfirmDialog2() {
    withTree = false; // Default checkbox not selected
    return showDialog<bool>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("Prompt"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Text("Are you sure you want to delete the current file?"),
              Row(
                children: <Widget>[
                  Text("Also delete subdirectories?"),
                  Checkbox(
                    value: withTree,
                    onChanged: (bool value) {
                      // Rebuild UI when the checkbox selection state changes
                      setState(() {
                        // Update checkbox state
                        withTree = !withTree;
                      });
                    },
                  ),
                ],
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text("Cancel"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("Delete"),
              onPressed: () {
                // Perform delete operation
                Navigator.of(context).pop(withTree);
              },
            ),
          ],
        );
      },
    );
  }
}

However, when we run the above code, we find that the checkbox cannot be selected at all! Why is this the case? The reason is quite simple. We know that the setState method only rebuilds the subtree of the current context. However, our dialog is not built within the _DialogRouteState's build method but is constructed separately through showDialog, so calling setState in _DialogRouteState's context cannot affect the UI constructed by showDialog.

Additionally, we can understand this phenomenon from another perspective: as mentioned earlier, dialogs are also implemented via routes, meaning that the above code is essentially attempting to call setState in the parent route to update the child route, which is clearly not possible! In short, the root cause is that the context is incorrect. So, how can we make the checkbox clickable? Typically, there are three methods:

1. Extracting a StatefulWidget

Since the context is incorrect, a direct approach is to encapsulate the checkbox selection logic into a separate StatefulWidget, thereby managing the selection state within it. Let’s look at this approach; below is the implementation code:

// A checkbox component that manages its own selection state
class DialogCheckbox extends StatefulWidget {
  DialogCheckbox({
    Key? key,
    this.value,
    required this.onChanged,
  });

  final ValueChanged<bool?> onChanged;
  final bool? value;

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

class _DialogCheckboxState extends State<DialogCheckbox> {
  bool? value;

  @override
  void initState() {
    value = widget.value;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Checkbox(
      value: value,
      onChanged: (v) {
        // Emit the selection state through an event
        widget.onChanged(v);
        setState(() {
          // Update own selection state
          value = v;
        });
      },
    );
  }
}

Here is the code for popping up the dialog:

Future<bool?> showDeleteConfirmDialog3() {
  bool _withTree = false; // Record if the checkbox is selected
  return showDialog<bool>(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text("Prompt"),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text("Are you sure you want to delete the current file?"),
            Row(
              children: <Widget>[
                Text("Also delete subdirectories?"),
                DialogCheckbox(
                  value: _withTree, // Default not selected
                  onChanged: (bool value) {
                    // Update selection state
                    _withTree = !_withTree;
                  },
                ),
              ],
            ),
          ],
        ),
        actions: <Widget>[
          TextButton(
            child: Text("Cancel"),
            onPressed: () => Navigator.of(context).pop(),
          ),
          TextButton(
            child: Text("Delete"),
            onPressed: () {
              // Return the selection state
              Navigator.of(context).pop(_withTree);
            },
          ),
        ],
      );
    },
  );
}

Finally, here’s how to use it:

ElevatedButton(
  child: Text("Dialog 3 (Checkbox Clickable)"),
  onPressed: () async {
    // Pop up the delete confirmation dialog and wait for user confirmation
    bool? deleteTree = await showDeleteConfirmDialog3();
    if (deleteTree == null) {
      print("Cancel deletion");
    } else {
      print("Also delete subdirectories: $deleteTree");
    }
  },
),

The results can be seen in Figure .

Flutter (55): Detailed explanation of dialog boxes

As observed, the checkbox can now be selected, and upon clicking "Cancel" or "Delete," the console will print the final confirmation state.

2. Using the StatefulBuilder Method

Although the above method solves the issue of updating the dialog state, it has a significant drawback: all components on the dialog that may change state must be encapsulated within a separate StatefulWidget that manages its own state. This not only complicates the code but also reduces reusability. Therefore, let’s consider whether there is a simpler approach. The previous method essentially places the dialog's state within the context of a StatefulWidget, which manages it internally. Is there a way to create a StatefulWidget context without needing to extract components separately?

Here, we can draw inspiration from the implementation of the Builder widget. As previously mentioned, the Builder widget can access the true context of its location. How is this achieved? Let’s take a look at its source code:

class Builder extends StatelessWidget {
  const Builder({
    Key? key,
    required this.builder,
  }) : assert(builder != null),
       super(key: key);
  final WidgetBuilder builder;

  @override
  Widget build(BuildContext context) => builder(context);
}

We can see that Builder merely extends StatelessWidget and, in its build method, retrieves the current context, delegating the build process to the builder callback. Thus, Builder effectively captures the context of StatelessWidget. Can we employ a similar method to acquire the context of StatefulWidget and delegate its build method? Let’s create a StatefulBuilder method to achieve this:

class StatefulBuilder extends StatefulWidget {
  const StatefulBuilder({
    Key? key,
    required this.builder,
  }) : assert(builder != null),
       super(key: key);

  final StatefulWidgetBuilder builder;

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

class _StatefulBuilderState extends State<StatefulBuilder> {
  @override
  Widget build(BuildContext context) => widget.builder(context, setState);
}

The code is straightforward. StatefulBuilder acquires the context of the StatefulWidget and proxies its build process. We can now refactor the above code using StatefulBuilder (the changes are only in the DialogCheckbox section):

... // Omitted unrelated code
Row(
  children: <Widget>[
    Text("Delete subdirectories too?"),
    // Using StatefulBuilder to build the context of StatefulWidget
    StatefulBuilder(
      builder: (context, _setState) {
        return Checkbox(
          value: _withTree, // Default unchecked
          onChanged: (bool value) {
            //_setState method is effectively the setState method of this StatefulWidget,
            // which will call the builder method again
            _setState(() {
              // Update the selection state
              _withTree = !_withTree;
            });
          },
        );
      },
    ),
  ],
),

Essentially, this approach allows child components to notify the parent component (StatefulWidget) to rebuild itself to update the UI, which readers can understand by comparing the code. In fact, StatefulBuilder is a class provided in the Flutter SDK, and it operates on the same principle as Builder. It is crucial for readers to thoroughly understand both StatefulBuilder and Builder, as they are highly practical in Flutter.

3. A Clever Solution

Is there an even simpler solution? To confirm this, we need to clarify how the UI is updated. We know that after calling the setState method, the StatefulWidget is rebuilt. So what does the setState method do? Can we find a method from it? Following this thought process, let's examine the core source code of setState:

void setState(VoidCallback fn) {
  ... // Omitted unrelated code
  _element.markNeedsBuild();
}

We can see that setState calls the markNeedsBuild() method of the Element. As previously mentioned, Flutter is a reactive framework, and to update the UI, one simply needs to change the state and notify the framework that the page needs to be rebuilt. The markNeedsBuild() method marks the current Element object as "dirty." In every frame, Flutter will rebuild any Element object marked as "dirty."

Given this, is there a way to obtain the Element object of the internal UI of the dialog and mark it as "dirty"? The answer is yes! We can access the Element object through the Context. Regarding the relationship between Element and Context, we will delve deeper into this in the chapter on "Core Principles of Flutter." For now, simply understand that in the component tree, the context is essentially a reference to the Element object. With this knowledge, a solution becomes evident: we can update the checkbox using the following method:

Future<bool?> showDeleteConfirmDialog4() {
  bool _withTree = false;
  return showDialog<bool>(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text("Prompt"),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text("Are you sure you want to delete the current file?"),
            Row(
              children: <Widget>[
                Text("Delete subdirectories too?"),
                Checkbox( // Still using the Checkbox component
                  value: _withTree,
                  onChanged: (bool value) {
                    // At this point, context is the root Element of the dialog UI. 
                    // We can directly mark the corresponding Element of the dialog UI as dirty.
                    (context as Element).markNeedsBuild();
                    _withTree = !_withTree;
                  },
                ),
              ],
            ),
          ],
        ),
        actions: <Widget>[
          TextButton(
            child: Text("Cancel"),
            onPressed: () => Navigator.of(context).pop(),
          ),
          TextButton(
            child: Text("Delete"),
            onPressed: () {
              // Execute the delete operation
              Navigator.of(context).pop(_withTree);
            },
          ),
        ],
      );
    },
  );
}

After running the above code, the checkbox can be selected normally. Notice how we resolved the issue with just one line of code! Of course, the code above is not optimal because we only need to update the checkbox's state. Currently, the context we are using is the root context of the dialog, which will cause the entire dialog UI component to rebuild. Therefore, the best approach is to narrow down the "scope" of the context, meaning we should only mark the Checkbox's Element as dirty. The optimized code would be:

... // Omitted unrelated code
Row(
  children: <Widget>[
    Text("Delete subdirectories too?"),
    // Using Builder to get the context for building the Checkbox, 
    // which is a common method to narrow the context's scope.
    Builder(
      builder: (BuildContext context) {
        return Checkbox(
          value: _withTree,
          onChanged: (bool value) {
            (context as Element).markNeedsBuild();
            _withTree = !_withTree;
          },
        );
      },
    ),
  ],
),

55.5 Other Types of Dialogs

1. Bottom Menu List

The showModalBottomSheet method can pop up a Material-style modal dialog featuring a bottom menu list, as shown in the example below:

// Pop up a bottom menu list modal dialog
Future<int?> _showModalBottomSheet() {
  return showModalBottomSheet<int>(
    context: context,
    builder: (BuildContext context) {
      return ListView.builder(
        itemCount: 30,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            title: Text("$index"),
            onTap: () => Navigator.of(context).pop(index),
          );
        },
      );
    },
  );
}

Clicking the button will pop up this dialog:

ElevatedButton(
  child: Text("Show Bottom Menu List"),
  onPressed: () async {
    int type = await _showModalBottomSheet();
    print(type);
  },
),

The result after running is shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

2. Loading Dialog

The loading dialog can actually be customized directly using showDialog + AlertDialog:

showLoadingDialog() {
  showDialog(
    context: context,
    barrierDismissible: false, // Do not close the dialog when clicking the overlay
    builder: (context) {
      return AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            CircularProgressIndicator(),
            Padding(
              padding: const EdgeInsets.only(top: 26.0),
              child: Text("Loading, please wait..."),
            )
          ],
        ),
      );
    },
  );
}

The display effect is shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

If the loading dialog is too wide and you want to customize its width, using just SizedBox or ConstrainedBox won't work because showDialog has already set a minimum width constraint for the dialog. As we discussed in Chapter 5 about "Size Limit Classes," we can use UnconstrainedBox to negate the width constraints set by showDialog, and then specify the width with SizedBox. The code is as follows:

... // Omitted unrelated code
UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: SizedBox(
    width: 280,
    child: AlertDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          CircularProgressIndicator(value: .8,),
          Padding(
            padding: const EdgeInsets.only(top: 26.0),
            child: Text("Loading, please wait..."),
          )
        ],
      ),
    ),
  ),
);

After running this code, the effect is shown in Figure :

Flutter (55): Detailed explanation of dialog boxes

3. Calendar Picker

First, let's look at the Material-style calendar picker, as shown in Figure :

fFlutter (55): Detailed explanation of dialog boxes

Implementation code:

Future<DateTime?> _showDatePicker1() {
  var date = DateTime.now();
  return showDatePicker(
    context: context,
    initialDate: date,
    firstDate: date,
    lastDate: date.add( // Future 30 days selectable
      Duration(days: 30),
    ),
  );
}

The iOS-style calendar picker requires the use of showCupertinoModalPopup method and CupertinoDatePicker component to implement:

Future<DateTime?> _showDatePicker2() {
  var date = DateTime.now();
  return showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
      return SizedBox(
        height: 200,
        child: CupertinoDatePicker(
          mode: CupertinoDatePickerMode.dateAndTime,
          minimumDate: date,
          maximumDate: date.add(
            Duration(days: 30),
          ),
          maximumYear: date.year + 1,
          onDateTimeChanged: (DateTime value) {
            print(value);
          },
        ),
      );
    },
  );
}

The running effect is shown in Figure :

Flutter (55): Detailed explanation of dialog boxes