flutter CheckedPopupMenuItem Keep menu open after selecting an item

2.6k Views Asked by At

I'm trying to get the menu stay open so i can select multiple categories at once, without snapping back to page after each selection.

It would have been perfect if it would function like Excel filter data (or Libreoffice Autofilter)

added screenshots

any ideas would be appreciated!

current app my goal example

code:

import 'package:flutter/material.dart';

void main() {   return runApp(MaterialApp(
    home: MenuDemo(),   )); }

class MenuDemo extends StatefulWidget {   const MenuDemo({Key key}) : super(key: key);

  static const String routeName = '/material/menu';

  @override   MenuDemoState createState() => new MenuDemoState(); }

class MenuDemoState extends State<MenuDemo> {   final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  final String _checkedValue1 = 'One';   final String _checkedValue2 = 'Two';   final String _checkedValue3 = 'Free';   final String
_checkedValue4 = 'Four';   List<String> _checkedValues;

  @override   void initState() {
    super.initState();
    _checkedValues = <String>[_checkedValue3];   }

  void showInSnackBar(String value) {
    _scaffoldKey.currentState
        .showSnackBar(new SnackBar(content: new Text(value)));   }

  void showCheckedMenuSelections(String value) {
    if (_checkedValues.contains(value))
      _checkedValues.remove(value);
    else
      _checkedValues.add(value);

    showInSnackBar('Checked $_checkedValues');   }

  bool isChecked(String value) => _checkedValues.contains(value);

  @override   Widget build(BuildContext context) {
    return new Scaffold(
        key: _scaffoldKey,
        body: Center(
          child: Container(
            width: 250.0,
            child: new ListTile(
                title: const Text(
                  'checklist menu',
                  textAlign: TextAlign.center,
                ),
                trailing: new PopupMenuButton<String>(
                    padding: EdgeInsets.zero,
                    onSelected: showCheckedMenuSelections,
                    itemBuilder: (BuildContext context) =>
                        <PopupMenuItem<String>>[
                          new CheckedPopupMenuItem<String>(
                              value: _checkedValue1,
                              checked: isChecked(_checkedValue1),
                              child: new Text(_checkedValue1)),
                          new CheckedPopupMenuItem<String>(
                              value: _checkedValue2,
                              //enabled: false,
                              checked: isChecked(_checkedValue2),
                              child: new Text(_checkedValue2)),
                          new CheckedPopupMenuItem<String>(
                              value: _checkedValue3,
                              checked: isChecked(_checkedValue3),
                              child: new Text(_checkedValue3)),
                          new CheckedPopupMenuItem<String>(
                              value: _checkedValue4,
                              checked: isChecked(_checkedValue4),
                              child: new Text(_checkedValue4))
                        ])),
          ),
        ));   } }
1

There are 1 best solutions below

0
On

You can create your own multi-select pop-up menu since using the default PopupMenu dismisses the dialog when an item has been selected.

class MultiSelectDialogItem<V> {
  const MultiSelectDialogItem(this.value, this.label);

  final V value;
  final String label;
}

class MultiSelectDialog<V> extends StatefulWidget {
  const MultiSelectDialog(
      {Key? key, required this.items, this.initialSelectedValues})
      : super(key: key);

  final List<MultiSelectDialogItem<V>> items;
  final Set<V>? initialSelectedValues;

  @override
  State<StatefulWidget> createState() => _MultiSelectDialogState<V>();
}

class _MultiSelectDialogState<V> extends State<MultiSelectDialog<V>> {
  final _selectedValues = <V>{};

  @override
  void initState() {
    super.initState();
    if (widget.initialSelectedValues != null) {
      _selectedValues.addAll(widget.initialSelectedValues!);
    }
  }

  void _onItemCheckedChange(V itemValue, bool checked) {
    setState(() {
      if (checked) {
        _selectedValues.add(itemValue);
      } else {
        _selectedValues.remove(itemValue);
      }
    });
  }

  void _onCancelTap() {
    Navigator.pop(context);
  }

  void _onSubmitTap() {
    Navigator.pop(context, _selectedValues);
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Select items'),
      contentPadding: const EdgeInsets.only(top: 12.0),
      content: SingleChildScrollView(
        child: ListTileTheme(
          contentPadding: const EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0),
          child: ListBody(
            children: widget.items.map(_buildItem).toList(),
          ),
        ),
      ),
      actions: <Widget>[
        ElevatedButton(
          child: const Text('Cancel'),
          onPressed: _onCancelTap,
        ),
        ElevatedButton(
          child: const Text('Ok'),
          onPressed: _onSubmitTap,
        )
      ],
    );
  }

  Widget _buildItem(MultiSelectDialogItem<V> item) {
    final checked = _selectedValues.contains(item.value);
    return CheckboxListTile(
      value: checked,
      title: Text(item.label),
      controlAffinity: ListTileControlAffinity.leading,
      onChanged: (checked) =>
          _onItemCheckedChange(item.value, checked ?? false),
    );
  }
}

Then to display the menu.

void _showMultiSelect(BuildContext context) async {
  // set value and label on MultiSelectDialogItem 
  final items = <MultiSelectDialogItem<int>>[
    const MultiSelectDialogItem(1, 'Item 1'),
    const MultiSelectDialogItem(2, 'Item 2'),
    const MultiSelectDialogItem(3, 'Item 3'),
  ];

  final selectedValues = await showDialog<Set<int>>(
    context: context,
    builder: (BuildContext context) {
      return MultiSelectDialog(
        items: items,
        // use Set to configure initial selected value
        // i.e. initialSelectedValues: const {1,2}  
        initialSelectedValues: null,
      );
    },
  );

  // Fetch selected items
  debugPrint('Selected Values: $selectedValues');
}

Demo

Complete sample

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void _showMultiSelect(BuildContext context) async {
    final items = <MultiSelectDialogItem<int>>[
      const MultiSelectDialogItem(1, 'Item 1'),
      const MultiSelectDialogItem(2, 'Item 2'),
      const MultiSelectDialogItem(3, 'Item 3'),
    ];

    final selectedValues = await showDialog<Set<int>>(
      context: context,
      builder: (BuildContext context) {
        return MultiSelectDialog(
          items: items,
          initialSelectedValues: null,
        );
      },
    );

    debugPrint('Selected Values: $selectedValues');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            _showMultiSelect(context);
          },
          child: const Text('Show Dropdown Multiselect'),
        ),
      ),
    );
  }
}

class MultiSelectDialogItem<V> {
  const MultiSelectDialogItem(this.value, this.label);

  final V value;
  final String label;
}

class MultiSelectDialog<V> extends StatefulWidget {
  const MultiSelectDialog(
      {Key? key, required this.items, this.initialSelectedValues})
      : super(key: key);

  final List<MultiSelectDialogItem<V>> items;
  final Set<V>? initialSelectedValues;

  @override
  State<StatefulWidget> createState() => _MultiSelectDialogState<V>();
}

class _MultiSelectDialogState<V> extends State<MultiSelectDialog<V>> {
  final _selectedValues = <V>{};

  @override
  void initState() {
    super.initState();
    if (widget.initialSelectedValues != null) {
      _selectedValues.addAll(widget.initialSelectedValues!);
    }
  }

  void _onItemCheckedChange(V itemValue, bool checked) {
    setState(() {
      if (checked) {
        _selectedValues.add(itemValue);
      } else {
        _selectedValues.remove(itemValue);
      }
    });
  }

  void _onCancelTap() {
    Navigator.pop(context);
  }

  void _onSubmitTap() {
    Navigator.pop(context, _selectedValues);
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Select items'),
      contentPadding: const EdgeInsets.only(top: 12.0),
      content: SingleChildScrollView(
        child: ListTileTheme(
          contentPadding: const EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0),
          child: ListBody(
            children: widget.items.map(_buildItem).toList(),
          ),
        ),
      ),
      actions: <Widget>[
        ElevatedButton(
          child: const Text('Cancel'),
          onPressed: _onCancelTap,
        ),
        ElevatedButton(
          child: const Text('Ok'),
          onPressed: _onSubmitTap,
        )
      ],
    );
  }

  Widget _buildItem(MultiSelectDialogItem<V> item) {
    final checked = _selectedValues.contains(item.value);
    return CheckboxListTile(
      value: checked,
      title: Text(item.label),
      controlAffinity: ListTileControlAffinity.leading,
      onChanged: (checked) =>
          _onItemCheckedChange(item.value, checked ?? false),
    );
  }
}