Using a modal dialog to choose a value in Flutter

We are going to describe how to show a modal dialog in Flutter. In this dialog, the user will be able to pick some values, that we will return to the original Widget. In the last blog post, we show how to select multiple categories from a list. We are going to work from there. There is a list of values from which we can pick multiple categories. In some cases, we do not want to keep the list visible after the user has chosen on or multiple values. That’s why we will show how to do the same with a dialog.

Creating the dialog

Let’s start with creating a dialog. In the dialog, we will show a list of checkboxes with the names of fruit. Before we create the dialog, let’s first create an overview screen with a button that will open the dialog and a list to display the selected categories. Since the selected categories are able to change, we are creating a StatefulWidget. For displaying the selected categories, we are going to use a simple ListView.

class CategorySelector extends StatefulWidget {
  final List<String> categories;

  CategorySelector(this.categories);

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

class _CategorySelectorState extends State<CategorySelector> {
  final List<String> selectedCategories = [];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          child: Text("Select Fruit"),
          onPressed: () async {
            List<String> categories = await showDialog(
              context: this.context,
              builder: (BuildContext context) {
                return new Dialog(
                    child: CategorySelectorDialog(
                        widget.categories, List.from(selectedCategories)));
              },
            );
            setState(() {
              selectedCategories.clear();
              selectedCategories.addAll(categories);
            });
          },
        ),
        Container(
          color: Colors.green,
          height: 2,
        ),
        SelectedCategories(selectedCategories)
      ],
    );
  }
}

To open a dialog, Flutter has a simple method, showDialog that we can use. As you could already see in the code above, we are calling that method with another Widget as child. That will be the content of the dialog. In the dialog we will maintain the selected items of a list. This means we are using another StatefulWidget. In a ListView we display all the categories with a checkbox. On clicking on the checkbox, we update the state and thus the selected categories in this Widget. We added a button that goes back to the previous Widget.

class CategorySelectorDialog extends StatefulWidget {
  final List<String> categories;
  final List<String> currentSelection;

  CategorySelectorDialog(this.categories, this.currentSelection);

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

class _CategorySelectorDialogState extends State<CategorySelectorDialog> {
  final List<String> _selectedCategories = [];

  @override
  void initState() {
    _selectedCategories.addAll(widget.currentSelection);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(maxHeight: 350, minHeight: 200),
      child: Column(
        children: [
          ListView.builder(
              shrinkWrap: true,
              itemCount: widget.categories.length,
              itemBuilder: (BuildContext context, int index) {
                return CheckboxListTile(
                  value: _selectedCategories.contains(widget.categories[index]),
                  onChanged: (bool? selected) {
                    setState(() {
                      if (selected == true) {
                        _selectedCategories.add(widget.categories[index]);
                      } else {
                        _selectedCategories.remove(widget.categories[index]);
                      }
                    });
                  },
                  title: Text(widget.categories[index]),
                );
              }),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  Navigator.pop(context, _selectedCategories);
                },
                child: Text("Done"),
              )
            ],
          ),
        ],
      ),
    );
  }
}

As you can see, we passed the _selectedCategories to the Navigator.pop method. This mean that the on the place where we called the showDialog we can access those values. We can await the value of the dialog to get the selected categories. Now we can use those values to set the current selected categories.

            ElevatedButton(
                onPressed: () {
                  Navigator.pop(context, _selectedCategories);
                },
                child: Text("Done"),
              )
            ],

Add cancel button and initial selection

Unfortunately, every time we open the dialog none of the items are selected. This means that the second time we open the dialog we have to update all the values. Another option we are going to add is a cancel button. This means you will not update the selection and hold the values from the first selection. To do this we are going to pass the current selection to the dialog. For this, we have to override the initState method and add the current selection to the list of selected widgets. For the cancel button, we can simply return the currentSelection passed to the Widget.

class CategorySelectorDialog extends StatefulWidget {
  final List<String> categories;
  final List<String> currentSelection;

  CategorySelectorDialog(this.categories, this.currentSelection);

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

class _CategorySelectorDialogState extends State<CategorySelectorDialog> {
  final List<String> _selectedCategories = [];

  @override
  void initState() {
    _selectedCategories.addAll(widget.currentSelection);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(maxHeight: 350, minHeight: 200),
      child: Column(
        children: [
          ListView.builder(
              shrinkWrap: true,
              itemCount: widget.categories.length,
              itemBuilder: (BuildContext context, int index) {
                return CheckboxListTile(
                  value: _selectedCategories.contains(widget.categories[index]),
                  onChanged: (bool? selected) {
                    setState(() {
                      if (selected == true) {
                        _selectedCategories.add(widget.categories[index]);
                      } else {
                        _selectedCategories.remove(widget.categories[index]);
                      }
                    });
                  },
                  title: Text(widget.categories[index]),
                );
              }),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  Navigator.pop(context, widget.currentSelection);
                },
                child: Text("Cancel"),
              ),
              ElevatedButton(
                onPressed: () {
                  Navigator.pop(context, _selectedCategories);
                },
                child: Text("Done"),
              )
            ],
          ),
        ],
      ),
    );
  }
}

Passing the current selection can be easily done in the parent Widget. As you can see we are going to pass a copy of the list instead of the list itself. This is to prevent the list from updating itself when we return the current selection and update the selectedCategories with the currentSelection.

child: new Dialog(
   child: CategorySelectorDialog(
        widget.categories, List.from(selectedCategories)),
 ),

Thanks for reading, full code on Github or if you just want to play around, the code can be found on this Dartpad. Personally, I do not like the initState method in the Dialog selector and having two Stateful Widgets for simply selecting some of the values. Luckily this can be solved with Hooks as we did before! In the next blog post, we will describe how to rewrite this example and use Flutter hooks instead!

2 Comments

    • Thank you for letting me know.
      I have update the dependencies and the project.
      The code on Github should be compilable now

Leave a Reply