Reputation: 6018
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
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
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
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
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