asherbret
asherbret

Reputation: 6018

How to set DropdownButton's value programmatically?

For example, in order to set the text in a TextFormField, I can use a TextEditingController:

textEditingController = TextEditingController()
...

TextFormField(
  controller: textEditingController
);
...

textEditingController.text = 'my text'; // This is where I can set the text in the TextFormField

Is there a similar way to programmatically set the selection in a DropdownButton? As far as I know, simply setting the value field in a DropdownButton won't suffice since the change won't be applied without calling the setState from the wrapping state object.

Upvotes: 1

Views: 5285

Answers (4)

Abhishek Kumar
Abhishek Kumar

Reputation: 2276

You can use ValueNotifier as given in example below:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomePage());
  }
}

const List<String> list = <String>['One', 'Two', 'Three', 'Four'];

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

  @override
  State<HomePage> createState() => _HomePage();
}

class _HomePage extends State<HomePage> {
  ValueNotifier<String> buttonClickedTimes = ValueNotifier<String>('One');
  String dropdownValue = list.first;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child:
                Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      DropdownMenu<String>(
        initialSelection: list.first,
        onSelected: (String? value) {
          // This is called when the user selects an item.
          setState(() {
            buttonClickedTimes.value = value!;
          });
        },
        dropdownMenuEntries:
            list.map<DropdownMenuEntry<String>>((String value) {
          return DropdownMenuEntry<String>(value: value, label: value);
        }).toList(),
      ),
      const SizedBox(height: 20),
      ValueListenableBuilder(
          builder: (context, value, child) => DropdownMenu<String>(
                initialSelection: buttonClickedTimes.value,
                onSelected: (String? value) {
                  // This is called when the user selects an item.
                  setState(() {
                    dropdownValue = value!;
                  });
                },
                dropdownMenuEntries:
                    list.map<DropdownMenuEntry<String>>((String value) {
                  return DropdownMenuEntry<String>(value: value, label: value);
                }).toList(),
              ),
          valueListenable: buttonClickedTimes),
    ])));
  }
}

Upvotes: 0

Pablo Barrera
Pablo Barrera

Reputation: 10963

I created a simplified DropdownButton to be able to use a controller, it can be used like this:

SimpleDropdownButton(
  values: _values,
  itemBuilder: (value) => Text(value),
  controller: _controller,
  onChanged: (value) => print(_controller.value),
)

Basically the SimpleDropdownButton wraps a DropdownButton and handles the creation of its DropdownItems according to the list of values received and according to the way you want to display these values.

If you don't set a controller, then the SimpleDropdownButton will handle the selected value like we always do with DropdownButton using setState().

If you do set a controller, then the SimpleDropdownButton starts listening to the controller to know when to call setState() to update the selected value. So, if someone selects an item (onChanged) the SimpleDropdownButton won't call setState() but will set the new value to the controller and the controller will notify the listeners, and one of these listeners is SimpleDropdownButton who will call setState() to update the selected value. This way, if you set a new value to the controller, SimpleDropdownButton will be notified. Also, since the value is always stored on the controller, it can accessed at anytime.


Here is the implementation, you may want to pass more parameters to the DropdownButton:

class SimpleDropdownButton<T> extends StatefulWidget {
  final List<T> values;
  final Widget Function(T value) itemBuilder;
  final SimpleDropdownButtonController<T> controller;
  final ValueChanged onChanged;

  SimpleDropdownButton(
      {this.controller,
      @required this.values,
      @required this.itemBuilder,
      this.onChanged});

  @override
  _SimpleDropdownButtonState<T> createState() =>
      _SimpleDropdownButtonState<T>();
}

class _SimpleDropdownButtonState<T> extends State<SimpleDropdownButton<T>> {
  T _value;

  @override
  void initState() {
    super.initState();
    if (widget.controller != null) {
      _value = widget.controller.value;
      widget.controller.addListener(() => setState(() {
            _value = widget.controller.value;
          }));
    }
  }

  @override
  void dispose() {
    widget.controller?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return DropdownButton(
      value: _value,
      items: widget.values
          .map((value) => DropdownMenuItem(
                value: value,
                child: widget.itemBuilder(value),
              ))
          .toList(),
      onChanged: (value) {
        if (widget.controller != null) {
          widget.controller.value = value;
        } else {
          setState(() {
            _value = value;
          });
        }
        widget.onChanged?.call(value);
      },
    );
  }
}

class SimpleDropdownButtonController<T> {
  List<VoidCallback> _listeners = [];
  T _value;

  SimpleDropdownButtonController([this._value]);

  get value => _value;

  set value(T value) {
    _value = value;
    _listeners?.forEach((listener) => listener());
  }

  void addListener(VoidCallback listener) => _listeners.add(listener);

  void close() => _listeners?.clear();
}

And an example to use it:

final _values = ["Value 1", "Value 2", "Value 3", "Value 4"];
final _controller = SimpleDropdownButtonController("Value 1");

@override
Widget build(BuildContext context) {
  print('build()');
  return Scaffold(
    appBar: AppBar(title: Text("SimpleDropdownButton")),
    floatingActionButton: FloatingActionButton(
      onPressed: () => _controller.value = "Value 3",
    ),
    body: SimpleDropdownButton(
      values: _values,
      itemBuilder: (value) => Text(value),
      controller: _controller,
      onChanged: (value) => print(_controller.value),
    ),
  );
}

Upvotes: 0

Kris
Kris

Reputation: 3361

If you separate the logic from your ui, and pass events through streams that are listened to by your ui, you can get around using setState and the logic is easier to work with.

StreamBuilder is a great widget that can simplify your ui code a lot if you get used to using it. Essentially, every time a new value passes through the stream, the builder function is re-run, and whatever was put into the stream, like a new dropdown button value, can be found in snapshot.data.

Here's an example:

in your ui, you might build the dropdown button like this:

      StreamBuilder<String>(
        stream: logicClass.dropdownValueStream,
        builder: (context, snapshot) {
          return DropdownButton(
            items: logicClass.menuItems,
            onChanged: logicClass.selectItem,
            value: snapshot.data,
          );
      })

and in the logic class you would build the stream like this:

  StreamController<String> _dropDownController = StreamController<String>();
  Stream<String> get dropdownValueStream => _dropDownController.stream;
  Function get selectItem => _dropDownController.sink.add;

Finally, if you want to do anything with this data, you can store it in the logic class as it passes through the stream. This is essentially separation of UI logic from business logic, or the Bloc pattern.

Upvotes: 0

asherbret
asherbret

Reputation: 6018

As @CopsOnRoad commented, there seem to be no shortcuts here and setState must be called in order to reflect the change in the DropdownButton's selected value. The problem is, setState is protected so I needed to go through some loops to make sure it was called when needed. I ended up doing this by implementing a notifier which the DropdownButton's state would be a listener of. Something along the lines of the following:

class MyStatefulWidget extends StatefulWidget {

  final _valueNotifier = ValueNotifier<String>(null);

  @override
  State<StatefulWidget> createState() => MyState(_valueNotifier);

  // This exposes the ability to change the DropdownButtons's value
  void setDropdownValue(String value) {
    // This will notify the state and will eventually call setState
    _valueNotifier.value = value;
  }
}

class MyState extends State<MyStatefulWidget> {
  String _selection;

  MyState(ValueNotifier<String> valueNotifier) {
    valueNotifier.addListener(() {
      setState(() {
        _selection = valueNotifier.value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return DropdownButton<String>(
      items: [
        DropdownMenuItem<String>(
          value: "1",
          child: Text(
            "1",
          ),
        ),
        DropdownMenuItem<String>(
          value: "2",
          child: Text(
            "2",
          ),
        )
      ],
      onChanged: (value) {
        setState(() {
          _selection = value;
        });
      },
      value: _selection,
    );
  }
}

Upvotes: 1

Related Questions