I have a fairly complex form where the drop down list changes based on previous selections. For example, if I select option Delivery or Pickup, the next drop down would be based on that selection and show either a store list or delivery options.
I have the below code and have a tried a few options, but the drop down doesn't seem to update based on selection, however I can't figure out why as it should be refreshed with the state change.
Any suggestion on the best approach for this? I thought it might be related to the Key needing to be unique but that doesn't seem to solve the problem and also causes other issues like clear of selected item when other fields change.
Question: How can you provide dynamic drop downs based on previous form field selection in Dart/Flutter?
DropDownInputField(
inputList: const [
'Delivery',
'Pickup',
],
onchanged: (selection) {
setState(() {
order.shippingOption = selection;
});
},
name: 'Shipping Option',
),
const SizedBox(
height: 20,
),
DropDownInputField(
inputList: retrieveList(order.shippingOption),
onchanged: (value) => order.deliveryOption = value,
name: 'Delivery Options',
),
Option generation Function
List<String> retrieveList(String shippingOption) {
switch (shippingOption.toLowerCase()) {
case "delivery":
return [
'Standard',
'Express',
];
break;
case "pickup":
return [
'Store 1',
'Store 2',
];
break;
State Class
class _ShippingFormScreenState extends State<ShippingFormScreen>
with SingleTickerProviderStateMixin {
TabController tabController;
Order order;
generation Function will decide the second dropdown items. But If you click to select the second drop down item 1st, it will through errors. To handle this situation, you need to update the second dropdown value as well. You can set the second dropdown value=null. Therefor, we need to use nullable String for selection value.
On First DropDownFiled onChanged make seceond dropdown value null.
DropDownInputField(
inputList: const [
'Delivery',
'Pickup',
],
onchanged: (selection) {
setState(() {
order.shippingOption = selection;
order.deliveryOption = null;
});
},
name: 'Shipping Option',
),
And second dropdown seems ok . But make sure to make those field as nullable.
I will encourage you to check this
You have to disable the bottom drop down based on top drop down using onChanged
From the documentation, Disabling like this:
If items or onChanged is null, the button will be disabled, the down arrow will be grayed out, and the disabledHint will be shown (if provided)
So we will disable bottom drop down and see if order changes.
You should change bottom drop down onChanged function to this
items: retrieveList(order.shippingOption),
onChanged: order.shippingOption == null
? null
: (value) {
setState(() {
order.deliveryOption = value!;
});
},
and change retrieveList to this:
List<String> retrieveList(String? shippingOption) {
if (shippingOption == null) return [];
switch (shippingOption.toLowerCase()) {
case "delivery":
return [
'Standard',
'Express',
];
case "pickup":
return [
'Store 1',
'Store 2',
];
default:
throw Exception('Unknown shipping option');
}
}
}
Full code of the widget
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> {
Order order = Order();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropDownInputField(
items: const [
'Delivery',
'Pickup',
],
onChanged: (value) {
setState(() {
order.shippingOption = value!;
});
},
value: order.shippingOption,
name: 'Shipping Option',
),
const SizedBox(
height: 20,
),
DropDownInputField(
items: retrieveList(order.shippingOption),
onChanged: order.shippingOption == null
? null
: (value) {
setState(() {
order.deliveryOption = value!;
});
},
name: 'Delivery Options',
value: order.deliveryOption,
),
],
),
),
);
}
List<String> retrieveList(String? shippingOption) {
if (shippingOption == null) return [];
switch (shippingOption.toLowerCase()) {
case "delivery":
return [
'Standard',
'Express',
];
case "pickup":
return [
'Store 1',
'Store 2',
];
default:
throw Exception('Unknown shipping option');
}
}
}
class Order {
String? shippingOption;
String? deliveryOption;
}
class DropDownInputField extends StatelessWidget {
const DropDownInputField({
Key? key,
required this.items,
required this.onChanged,
required this.value,
required this.name,
}) : super(key: key);
final List<String> items;
final ValueChanged<String?>? onChanged;
final String? value;
final String name;
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: value,
hint: Text(name),
items: <DropdownMenuItem<String>>[
...items.map((e) => DropdownMenuItem(
child: Text(e),
value: e,
))
],
onChanged: onChanged,
);
}
}
Related
I have a list of reasons stored in my form controller using GetX
class formController extends GetxController {
String rejectreason1 = "Overheat";
List reasons = [
"Broken",
"Missing",
"Wet Wiring",
"Overheat",
"Orange",
"Obergene"
];
Widget Class:
class ReasonForm extends StatelessWidget {
formController formC = Get.put(formController());
ReasonForm(this.index, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
// TODO: implement build
return Form(
// key: formKey1,
child: Column(children: [
DropdownButtonFormField<String>(
decoration: const InputDecoration(icon: Icon(Icons.event)),
value: "Overheat",
icon: const Icon(Icons.arrow_downward),
style: const TextStyle(color: Colors.deepPurple),
items: formC.reasons.map((value) {
return DropdownMenuItem(
value: value.toString(),
child: Text(value),
);
}).toList(),
onChanged: (value) {
formC.rejectreason1 = value!;
print("Selected ${value}");
},
),
]),
);
}
}
and in the dropdown list with it being mapped when onChanged is called all the values can be stored and printed out but for some reason the item "Overheat" doesn't work. Other words such as "Others" also doesn't work either, but all the rest like orange, missing and others can be selected and printed out in the console.
I think its because you put value: "Overheat", so you can't change it because onTap function change formC.rejectreason1 but you dont put that value on your dropdown. Try to change it to value: formC.rejectreason1
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
I try to create a dropdown and to populate it with few objects which represents few servers from where the user can pick one, but when I run the app I'm getting an error saying:
The following assertion was thrown building DropdownWidget(dirty, state: _DropdownWidgetState#1f58f): There should be exactly one item with [DropdownButton]'s value: Instance of 'ServerModel'. Either zero or 2 or more [DropdownMenuItem]s were detected with the same value
Can you please help me to identify what I'm doing wrong in my code?
import 'package:flutter/material.dart';
class ServerSettingsPage extends StatefulWidget {
#override
_ServerSettingsPageState createState() => _ServerSettingsPageState();
}
class _ServerSettingsPageState extends State<ServerSettingsPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Server Settings")),
body: _buildUI(),
);
}
Widget _buildUI() {
return Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: Center(
child: Column(
children: <Widget>[
Text(
'Select a server:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
DropdownWidget(),
],
),
),
);
}
}
class DropdownWidget extends StatefulWidget {
DropdownWidget({Key key}) : super(key: key);
#override
_DropdownWidgetState createState() => _DropdownWidgetState();
}
class _DropdownWidgetState extends State<DropdownWidget> {
ServerModel dropdownValue =
ServerModel(name: 'Default', url: 'https://defaultServer.com/');
#override
Widget build(BuildContext context) {
return DropdownButton<ServerModel>(
value: dropdownValue,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.purple[700]),
underline: Container(
height: 2,
color: Colors.purple[700],
),
onChanged: (ServerModel newServer) {
setState(() {
dropdownValue = newServer;
});
},
items: <ServerModel>[
ServerModel(name: 'Default', url: 'https:defaultServer.com/'),
ServerModel(name: 'Alpha', url: 'https://alphaServer.com/'),
ServerModel(name: 'Beta', url: 'https://betaServer.com/'),
].map<DropdownMenuItem<ServerModel>>((ServerModel server) {
return DropdownMenuItem<ServerModel>(
value: server,
child: Text(server.name, style: TextStyle(fontSize: 20)),
);
}).toList(),
);
}
}
And here is the ServerModel class:
class ServerModel {
ServerModel({this.name, this.url});
ServerModel.empty() {
this.name = null;
this.url = null;
}
String name;
String url;
}
Many thanks for reading this post.
There should be exactly one item with [DropdownButton]'s value:
Instance of 'ServerModel'. Either zero or 2 or more
[DropdownMenuItem]s were detected with the same value
This is happening because selected value inside the dropdown has to point to an existing list item (and obviously there shouldn't be any duplicates in that list). The way you've set it up right now is that the list of ServerModel is being generated during your widget build time and once it is built there no reference to the list inside the state of the widget.
I hope my answer is clear enough, also take a look at correct code bellow:
class _DropdownWidgetState extends State<DropdownWidget> {
List<ServerModel> serverModels = <ServerModel>[
ServerModel(name: 'Default', url: 'https:defaultServer.com/'),
ServerModel(name: 'Alpha', url: 'https://alphaServer.com/'),
ServerModel(name: 'Beta', url: 'https://betaServer.com/'),
];
ServerModel selectedServer;
#override
initState() {
super.initState();
selectedServer = serverModels[0];
}
#override
Widget build(BuildContext context) {
return DropdownButton<ServerModel>(
value: selectedServer,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.purple[700]),
underline: Container(
height: 2,
color: Colors.purple[700],
),
onChanged: (ServerModel newServer) {
setState(() {
selectedServer = newServer;
});
},
items: serverModels.map((ServerModel map) {
return new DropdownMenuItem<ServerModel>(
value: map, child: Text(map.name));
}).toList(),
);
}
}
Tested, working interactive answer on dartpad:
https://dartpad.dev/153bad9baac64382e27bc41cdc8131c9
You're facing an equality problem.
In Dart, non-primitive types like SizedBox, List, and in your case, ServerModel are compared to each other using referential equality, meaning that they are equal to each other if they have the same reference. That is, they are the same instance.
So this code will print false:
print(ServerModel(name: 'Default', url: 'https://defaultServer.com/') == ServerModel(name: 'Default', url: 'https://defaultServer.com/'));
// TL;DR
print(ServerModel(xyz) == ServerModel(xyz)); // false
The solution would be to override the equality operator for your class ServerModel.
class ServerModel {
ServerModel({this.name, this.url});
ServerModel.empty() {
this.name = null;
this.url = null;
}
String name;
String url;
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ServerModel && other.name == name && other.url == url;
}
#override
int get hashCode => name.hashCode ^ url.hashCode;
}
Now it should work.
PRO TIP: Use equatable to automatically generate equality and hashcode.
I have to generate TextFormField under a Form widget based on an array return from the API. Once these fields are generated the can enter values in it. When the user clicks on the submit button, the values of each field should be put in an array to post the API.
Object to be sent to API
{
"billerId" :"12341249",
"customerParams": [ // Each object in this array denotes input field's name and value
{
"name": "Consumer Number",
"value" : "43141"
},
{
"name": "Subdivision Code",
"value": "23"
}
]
}
Below is my StatefulWidget where I'm looping the over fields array (which came from API) to generate fields. My problem is when I'm hitting the Submit button, the print statement logs the object, similar to above, but the last field pushed twice.
// All necessary imports
class AddCustomerDetails extends StatefulWidget {
final Biller biller;
const AddCustomerDetails({Key key, this.biller}) : super(key: key);
#override
_AddCustomerDetailsState createState() => _AddCustomerDetailsState();
}
class _AddCustomerDetailsState extends State<AddCustomerDetails> {
final _formKey = GlobalKey<FormState>();
List _customerInputFields;
var _submitObj;
#override
void initState() {
_customerInputFields = widget.biller.customerParameter;
_submitObj = {'billerId': widget.biller.id, 'customerParams': []}; // Initializing it here
super.initState();
}
Widget _generateForm(List fields) {
return Form(
key: _formKey,
child: Column(
children: [
...fields.map((field) {
return TextFormField(
validator: (value) => _validateField(value),
onChanged: (value) {
_submitObj['customerParams']
.add({'name': field['paramName'], 'value': value}); // I know this is wrong and will push object on every key press
},
);
}).toList(),
SizedBox(height: 16),
RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
print(_submitObj); // See Actual response in snippet below
}
},
child: Text('Submit'),
),
],
),
);
}
String _validateField(value) {
// ... Validate field if empty
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: EdgeInsets.all(
AppMediaQuery(context).appHorizontalPadding(4),
),
child: _generateForm(_customerInputFields),
),
],
),
);
}
}
Actual response
I/flutter ( 8118): {billerId: JBVNL0000JHA01, customerParams: [{name: Consumer Number, value: 4}, {name: Consumer Number, value: 43}, {name: Consumer Number, value: 431}, {name: Consumer Number, value: 4314}, {name: Consumer Number, value: 43141}, {name: Subdivision Code, value: 2}, {name: Subdivision Code, value: 23}]}
I don't want it to push the object every time I press the key.
How this can be achieved? Of course, I can add a denounce, but that won't solve the problem.
Could you use a Map to collect the values instead of a List?
void initState() {
_customerInputFields = widget.biller.customerParameter;
/// Initialize `customerParams` as a map here
_submitObj = {'billerId': widget.biller.id, 'customerParams': {}};
super.initState();
}
...
return TextFormField(
validator: (value) => _validateField(value),
onChanged: (value) {
/// Add/Update the field subscript and value here
_submitObj['customerParams'][field['paramName']] = value;
},
);```
for an authentication I would like to recover the base_url of the company chosen from a drop-down list, but I can't do it, being a beginner a little help will be welcome.
here is the code of the dropdownlist:
class DropDown extends StatefulWidget {
DropDown({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<DropDown> {
String _mySelection;
String _myBaseUrl;
List<Map> _myJson = [{"id":2,"society":"test","baseUrl":"url.com"},{"id":1,"society":"planeef","baseUrl":"url.com"}];
#override
Widget build(BuildContext context) {
return Container(
child: new DropdownButton<String>(
isDense: true,
hint: new Text("Select"),
value: _mySelection,
onChanged: (String newValue) {
setState(() {
_mySelection = newValue;
});
},
items: _myJson.map((Map map) {
return new DropdownMenuItem<String>(
value: map["id"].toString(),
child: new Text(
map["society"],
),
);
}).toList(),
),
);
}
}
Check the code below. You can use singleWhere function to retrieve the element from the id value you are getting from the dropdown and then read baseUrl from the element.
The singleWhere function matches and returns a single element from the list based on the condition we provide.
Note -
The singleWhere function throws an error by default if there are duplicates or no element is found. You might need to also pass the orElse parameter to singleWhere or add some error handling in that case.
More about that can be found here.
class _MyHomePageState extends State<MyHomePage> {
String _mySelection;
List<Map> _myJson = [{"id":2,"society":"test","baseUrl":"url.com"},{"id":1,"society":"planeef","baseUrl":"url.com"}];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: new DropdownButton<String>(
isDense: true,
hint: new Text("Select"),
value: _mySelection,
onChanged: (String newValue) {
Map<dynamic,dynamic> _myElement = _myJson.singleWhere((test) => test["id"] == int.parse(newValue));
print(_myElement["baseUrl"]);
//Add the above two lines
setState(() {
_mySelection = newValue;
});
},
items: _myJson.map((Map map) {
return new DropdownMenuItem<String>(
value: map["id"].toString(),
child: new Text(
map["society"],
),
);
}).toList(),
),
)
);
}
}