AhmedLSayed9 / dropdown_button2

Flutter's core Dropdown Button widget with steady dropdown menu and many other features.
https://pub.dev/packages/dropdown_button2
MIT License
264 stars 122 forks source link

Cannot simultaneously update items and values in beta #261

Closed Colton127 closed 4 months ago

Colton127 commented 4 months ago

If you update both items and selected value simultaneously, whereby the new items do not contain the previously selected value, the following error is thrown:

_AssertionError ('package:dropdown_button2/src/utils.dart': Failed assertion: line 93 pos 5: 'valueListenable?.value == null || items.where((DropdownItem item) { return item.value == valueListenable!.value; }).length == 1': There should be exactly one item with [DropdownButton]'s value: 162369. Either zero or 2 or more [DropdownItem]s were detected with the same value)

See reproducible example below. It appears that dropdown_button is rebuilt with the new items before the ValueNotifier is updated with the new value.

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final random = Random();
  late int selectedValue;
  late List<DropdownItem<int>> dropDownitems;

  @override
  void initState() {
    _generateItems();
    super.initState();
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'DropDown Test',
        home: Scaffold(
          appBar: AppBar(
            title: const Text('DropDown Test'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                DropDownWidget(
                  selectedValue,
                  items: dropDownitems,
                ),
                OutlinedButton(
                    onPressed: () {
                      setState(() {
                        selectedValue = dropDownitems[random.nextInt(dropDownitems.length)].value!;
                      });
                    },
                    child: const Text('Select random value')),
                OutlinedButton(
                    onPressed: () {
                      setState(() {
                        _generateItems();
                      });
                    },
                    child: const Text('Generate items')),
              ],
            ),
          ),
        ));
  }

  void _generateItems() {
    dropDownitems = List.generate(10, (i) {
      final value = random.nextInt(500000);
      return DropdownItem(value: value, child: Text(value.toString()));
    });
    selectedValue = dropDownitems[random.nextInt(dropDownitems.length)].value!;
  }
}

class DropDownWidget extends StatefulWidget {
  final int value;
  final List<DropdownItem<int>> items;
  const DropDownWidget(this.value, {required this.items, super.key});

  @override
  State<DropDownWidget> createState() => _DropDownWidgetState();
}

class _DropDownWidgetState extends State<DropDownWidget> {
 late var selectedValueListenable = ValueNotifier<int>(widget.value);

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

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

  @override
  void didUpdateWidget(DropDownWidget oldWidget) {
    if (oldWidget.value != widget.value) {
      print('selecting value: ${widget.value}. items: ${widget.items.map((e) => e.value).toList()}');
      selectedValueListenable.value = widget.value;
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 400,
      height: 100,
      child: DropdownButtonHideUnderline(
        child: DropdownButton2<int>(
          isExpanded: true,
          items: widget.items,
          valueListenable: selectedValueListenable,
          onChanged: (value) {},
        ),
      ),
    );
  }
}

Output: selecting value: 65601. items: [350816, 127516, 289448, 65601, 376364, 295008, 451829, 219180, 496107, 317415]

Selected value is contained within item list values despite error being thrown.

Temporary fix is to create a new ValueNotifier every time items are updated:

  @override
  void didUpdateWidget(DropDownWidget oldWidget) {
    if (oldWidget.items != widget.items) {
      selectedValueListenable.dispose();
      selectedValueListenable = ValueNotifier<int>(widget.value);
    } else if (oldWidget.value != widget.value) {
      selectedValueListenable.value = widget.value;
    }
    super.didUpdateWidget(oldWidget);
  }

Thank you

AhmedLSayed9 commented 4 months ago

It's a matter of race condition in your code.

When you do selectedValueListenable.value = widget.value;, it will trigger the internal listener (which updates the selected index) and that listener will be using the old items as DropdownButton2 is not built with the new items yet "[build] is being called after [didUpdateWidget]".

You should instead do:

  @override
  void didUpdateWidget(DropDownWidget oldWidget) {
    if (oldWidget.value != widget.value) {
      selectedValueListenable.dispose();
      selectedValueListenable = ValueNotifier<int>(widget.value);
    }
    super.didUpdateWidget(oldWidget);
  }
AhmedLSayed9 commented 4 months ago
  @override
  void didUpdateWidget(DropDownWidget oldWidget) {
    if (oldWidget.items != widget.items) {
      selectedValueListenable.dispose();
      selectedValueListenable = ValueNotifier<int>(widget.value);
    } else if (oldWidget.value != widget.value) {
      selectedValueListenable.value = widget.value;
    }
    super.didUpdateWidget(oldWidget);
  }

is also valid