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.
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,
);
}
}
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),
),
);
}
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.
Related
I tried to figure it out, and read the documentation for both but I didn't find an answer, here is an example of what I mean:
List<String> items = ["item1", "item2", "item3", "item4"];
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
String selectedItem = items[0];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: DropdownButton(
value: selectedItem,
onChanged: (value) => selectedItem = value!,
items: items
.map(
(e) => DropdownMenuItem<String>(
value: e,
child: Text(e),
),
)
.toList(),
),
...
that's just a simple stateless widget with a DropdownButton at the center:
output of the code above
if we just change the widget to a DropdownButtonFormField with all else remains the same, changes to the selected item reflect in the UI: output of the same code after changing the widget to a DropdownButtonFormField
If you dig inside DropdownButtonFormField you will see it keeps a separate value for the menu inside its state. If you explore the code it says
onChanged: onChanged == null ? null : state.didChange,
state.didChange looks like:
#override
void didChange(T? value) {
super.didChange(value);
final DropdownButtonFormField<T> dropdownButtonFormField = widget as DropdownButtonFormField<T>;
assert(dropdownButtonFormField.onChanged != null);
dropdownButtonFormField.onChanged!(value);
}
and super.didChange looks like
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.of(context)?._fieldDidChange();
}
This changes the iternal value of the state and calls setState so that it refreshes the UI for it.
As a result, even if you change this line of your code:
onChanged: (value) => selectedItem = value!,
to
onChanged: (value){},
It still works visually, because it doesn't actually use selectedItem but the internal value.
Apologies in advance for posting Pseudo code. Real code would be too long.
I have a screen where I have a drop down at the top where a user can select an option. The rest of the page updates based on that option. Something like this:
// state variable
String idFromDropdown;
Column(
children: [
DropDownWidget(),
ChildWidget1(myId: idFromDropDown),
ChildWidget2(myId: idFromDropDown),
ChildWidget3(myId: idFromDropDown),
]
)
In the child widgets, I am using widget.myId to pass into a backend service and read new data.
Expectation is that when the dropdown changes and I call
setState((val)=>{idFromDropdown = val});
then the value would cascade into the three child widgets. Somehow trigger the widgets to reconnect to the backend service based on the new value of widget.myId.
How do I trigger a state update on the child widgets?
I ended up using a ValueNotifier. Instead of directly using a string and passing that into the child widgets. I ended up doing something like:
ValueNotifier<String> idFromDropdown;
...
setState((val)=>{idFromDropdown.value = val});
Then in each widget, I am adding a listener onto the ValueNotifier coming in and retriggering the read to the backend service.
While this works, I feel like I'm missing something obvious. My childwidgets now take in a ValueNotifier instead of a value. I'm afraid this is going to make my ChildWidgets more difficult to use in other situations.
Is this the correct way of doing this?
Use provider package your problem will solved easily
Here is example of Riverpod.
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final fetureDataForChild =
FutureProvider.family<List<String>, String>((ref, id) {
return Future.delayed(Duration(seconds: 1), () {
return <String>["active", "${id}", "got it "];
});
});
class MainWidgetR extends StatefulWidget {
#override
_MainWidgetState createState() => _MainWidgetState();
}
class _MainWidgetState extends State<MainWidgetR> {
String id = "id 0";
final items = List.generate(
4,
(index) => DropdownMenuItem(
child: Text("Company $index"),
value: "id $index",
),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButton(
items: items,
value: id,
onChanged: (value) {
setState(() {
id = value as String;
});
},
),
RiverPodRespone(
id: id,
),
],
),
),
);
}
}
class RiverPodRespone extends ConsumerWidget {
final String id;
RiverPodRespone({
required this.id,
});
#override
Widget build(BuildContext context, watch) {
final futureData = watch(fetureDataForChild("$id"));
return futureData.map(
data: (value) {
final items = value.value;
return Column(
children: [
...items.map((e) => Text("$e")).toList(),
],
);
},
loading: (value) => CircularProgressIndicator(),
error: (value) => Text(value.toString()),
);
}
}
I have this widget:
DropdownButton<String>(
value: rentPeriod,
items: rentPeriods.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(translate("expense.$value")),
);
}).toList(),
onChanged: (value) async {
setState(() {
rentPeriod = value;
});
},
),
How can I disable, let's say, the first option of the list?
i dont think there is any straight forward way of disabling a DropdownMenuItem
but you can have a list of the DropdownMenuItems you want to disable and then when you run setState you can check if that DropdownMenuItem is contained in that list and if it is then do nothing, also check by the DropdownMenuItem text if its contained in that list and if it is then change the color to be greyed out.
Like this
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
var rentPeriods = <String>['one', 'two'];
final disabledItems = ['one'];
var rentPeriod;
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: rentPeriod,
items: rentPeriods.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
translate("expense.$value"),
style: TextStyle(
color: disabledItems.contains(value) ? Colors.grey : null,
),
),
);
}).toList(),
onChanged: (value) async {
if (!disabledItems.contains(value)) {
setState(() {
rentPeriod = value;
});
}
},
);
}
}
You can create your own disable customization, changing the color and the callback of onChangedfunction in the DropdownButton, like this example:
https://dartpad.dev/587b44d2f1b06e056197fcf705021699?null_safety=true
Framework & Architecture
I have a specific architecture in my Flutter app. I am using BLoC pattern (flutter_bloc) to maintain state and fetch data from remote server.
How autocomplete should behave
I want to build autocomplete input. When user types, it starts fetching data from server after few milliseconds. As user types, the list of suggestions should be updated from the remote server and shown with filtered values to the user. Additionally, I need to set initial value of the autocomplete text field if such value is present 1. The way data is presented is also custom. Suggestion list presents user with suggestions containing both name and id values but text field can only contain name value (this name value is also used for searching the suggestions) 2.
I am not having much luck when using RawAutocomplete widget from Flutter material library. I have succeeded in making the initial value appear in the field by leveraging TextEditingController and didUpdateWidget method. The problem is, when I'm typing into the field, the suggestions are being fetched and passed to the widget but the suggestion list (built via optionsViewBuilder) is not being built. Usually the list appears if I change value in the field but that's too late to be useful.
This is what I have tried:
Link to live demo
NOTE: Try typing "xyz", that is a pattern that should match one of the suggestions. Waiting a bit and deleting single character will show the suggestions.
I am attaching two components as an example. Parent component called DetailPage takes care of triggering fetch of the suggestions and also stores selected suggestion / value of the input. Child component DetailPageForm contains actual input.
The example is artificially constrained but it is in regular MaterialApp parent widget. For brevity I'm not including BLoC code and just using regular streams. The code runs fine and I created it specifically for this example.
DetailPage
import 'dart:async';
import 'package:flutter/material.dart';
import 'detail_page_form.dart';
#immutable
class Suggestion {
const Suggestion({
this.id,
this.name,
});
final int id;
final String name;
}
class MockApi {
final _streamController = StreamController<List<Suggestion>>();
Future<void> fetch() async {
await Future.delayed(Duration(seconds: 2));
_streamController.add([
Suggestion(id: 1, name: 'xyz'),
Suggestion(id: 2, name: 'jkl'),
]);
}
void dispose() {
_streamController.close();
}
Stream<List<Suggestion>> get stream => _streamController.stream;
}
class DetailPage extends StatefulWidget {
final _mockApi = MockApi();
void _fetchSuggestions(String query) {
print('Fetching with query: $query');
_mockApi.fetch();
}
#override
_DetailPageState createState() => _DetailPageState(
onFetch: _fetchSuggestions,
stream: _mockApi.stream,
);
}
class _DetailPageState extends State<DetailPage> {
_DetailPageState({
this.onFetch,
this.stream,
});
final OnFetchCallback onFetch;
final Stream<List<Suggestion>> stream;
/* NOTE: This value can be used for initial value of the
autocomplete input
*/
Suggestion _value;
_handleSelect(Suggestion suggestion) {
setState(() {
_value = suggestion;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: StreamBuilder<List<Suggestion>>(
initialData: [],
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.red,
),
child: Flex(
direction: Axis.horizontal,
children: [ Text(snapshot.error.toString()) ]
)
);
}
return DetailPageForm(
list: snapshot.data,
value: _value != null ? _value.name : '',
onSelect: _handleSelect,
onFetch: onFetch,
);
}));
}
}
DetailPageForm
import 'dart:async';
import 'package:flutter/material.dart';
import 'detail_page.dart';
typedef OnFetchCallback = void Function(String);
typedef OnSelectCallback = void Function(Suggestion);
class DetailPageForm extends StatefulWidget {
DetailPageForm({
this.list,
this.value,
this.onFetch,
this.onSelect,
});
final List<Suggestion> list;
final String value;
final OnFetchCallback onFetch;
final OnSelectCallback onSelect;
#override
_DetailPageFormState createState() => _DetailPageFormState();
}
class _DetailPageFormState extends State<DetailPageForm> {
Timer _debounce;
TextEditingController _controller = TextEditingController();
FocusNode _focusNode = FocusNode();
List<Suggestion> _list;
#override
void initState() {
super.initState();
_controller.text = widget.value ?? '';
_list = widget.list;
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
void didUpdateWidget(covariant DetailPageForm oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
_controller = TextEditingController.fromValue(TextEditingValue(
text: widget.value,
selection: TextSelection.fromPosition(TextPosition(offset: widget.value.length)),
));
}
if (oldWidget.list != widget.list) {
setState(() {
_list = widget.list;
});
}
}
void _handleInput(String value) {
if (_debounce != null && _debounce.isActive) {
_debounce.cancel();
}
_debounce = Timer(const Duration(milliseconds: 300), () {
widget.onFetch(value);
});
}
#override
Widget build(BuildContext context) {
print(_list);
return Container(
padding: const EdgeInsets.all(10.0),
child: RawAutocomplete<Suggestion>(
focusNode: _focusNode,
textEditingController: _controller,
optionsBuilder: (TextEditingValue textEditingValue) {
return _list.where((Suggestion option) {
return option.name
.trim()
.toLowerCase()
.contains(textEditingValue.text.trim().toLowerCase());
});
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) {
return TextFormField(
controller: textEditingController,
focusNode: focusNode,
onChanged: _handleInput,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
);
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: SizedBox(
height: 200.0,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text('${option.id} ${option.name}'),
),
);
},
),
),
),
);
},
onSelected: widget.onSelect,
));
}
}
On the image you can see right at the end that I have to delete one letter to get the suggestions to show up.
Expected behaviour
I expect the suggestions list to be re-built every time new suggestions are available and provide them to the user.
1 The reason for that being that the input should show user a value that was selected before. This value might also be stored on the device. So the input is either empty or pre-filled with the value.
2 This example is constrained but basically text field should not contain the same text that the suggestions contain for specific reasons.
I solved this by calling the notifyListeners method which exists on the TextEditingController while I was setting the suggestions to the state.
setState(() {
_isFetching = false;
_suggestions = suggestions.sublist(0, min(suggestions.length, 5));
_searchController.notifyListeners();
});
The linter did say I should be implementing the ChangeNotifier class onto the Widget, but In this case I did not have to, it worked without it.
Move your _handleInput inside optionsBuilder becucaue the latter is called first.
optionsBuilder: (TextEditingValue textEditingValue) {
_handleInput(textEditingValue.text); // await if necessary
return _list.where((Suggestion option) {
return option.name
.trim()
.toLowerCase()
.contains(textEditingValue.text.trim().toLowerCase());
});
},
In 'optionsViewBuilder' in DetailsFormPage you need to pass _list instead of options.
I have a parent widget which calls a custom Switch widget that I've made. I need the value of the switch(whether it's ON or OFF) in my parent widget. Can I create a controller in my switch widget which would return that value?
Currently, I am passing the function from my parent widget which changes the value of a boolean which is in my parent widget based on the value of the switch in the switch widget.
Parent widget:
bool isSwitchOn = false;
Switch(onSwitchToggle: (bool val) {
isSwitchOn = val;
})
Custom Switch widget:
class Switch extends StatefulWidget {
Widget build(BuildContext context) {
return CupertinoSwitch(
value: widget.value,
onChanged: (bool value) {
setState(() {
widget.value = value;
});
widget.onSwitchToggle(value);
},
),
}
The switch widget is used everywhere in the code whenever I need a switch and sometimes I don't need to know the state of the switch and I just need to execute a function when the switch is toggled but the way I've written the code, I'll need to pass bool everywhere whenever I call the switch. Looking for a better way to do it.
eg: Bool val is unnecessary because i won't need it.
Switch(onSwitchToggle: (bool val) {
print('abc')
})
You can easily solve it by using a ChangeNotifier. That's actually also the way it's solved in TextFieldController and ScrollController.
I have some sample code for you based on what you described:
class SwitchController extends ChangeNotifier {
bool isSwitchOn = false;
void setValue(bool value) {
isSwitchOn = value;
notifyListeners();
}
}
Now your widget that wraps the Switch:
class CustomSwitch extends StatefulWidget {
CustomSwitch({
required this.controller
});
final SwitchController controller;
#override
State<StatefulWidget> createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch> {
#override
Widget build(BuildContext context) {
return CupertinoSwitch(
onChanged: (bool value) {
widget.controller.setValue(value);
},
value: widget.controller.isSwitchOn,
);
}
}
Just listen to the change event of the native switch and set the value of the controller. Then notify the observers about it.
Then you can create the widget and pass a controller you add a listener to:
class SwitchTest extends StatefulWidget {
#override
_SwitchTestState createState() => _SwitchTestState();
}
class _SwitchTestState extends State<SwitchTest> {
SwitchController controller = SwitchController();
#override
void initState() {
controller.addListener(() {
setState(() {
print(controller.isSwitchOn);
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
CustomSwitch(
controller: controller
),
Text(controller.isSwitchOn.toString()),
],
),
),
);
}
}
I have written a detailed tutorial on how to create such custom controllers on my blog: https://www.flutterclutter.dev/flutter/tutorials/create-a-controller-for-a-custom-widget/2021/2149/