Generate TextEditingController for dynamicly generated TextFormField in Flutter - flutter

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;
},
);```

Related

Flutter Dropdownmenu, certain items in array cannot be outputed

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

Dynamic Drop Down Field based on selection

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,
);
}
}

How do I get a child widget to update when a parent's state changes?

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()),
);
}
}

Can't update list with setState() in Flutter

I have a list of objects that I can display in a ListView. Now I wanted to implement a search feature and only display the search result. When I try to do it using onChanged on TextField(or even Controller) it doesn't work. I tried to debug and he gets the list updated correctly but he doesn't update the Widget. But when I removed the onChanged and added a button and then called the same method that I was calling on onChanged everything worked.
The goal is to update the widget as the user writes in the text field.
I would be happy to get some help
My full code :
import 'package:flutter/material.dart';
import 'package:hello_fridge/single_ingredient_icon.dart';
import 'package:string_similarity/string_similarity.dart';
import 'entities/ingredient.dart';
class IngredientsContainer extends StatefulWidget {
const IngredientsContainer({Key? key}) : super(key: key);
#override
_IngredientsContainerState createState() => _IngredientsContainerState();
}
class _IngredientsContainerState extends State<IngredientsContainer> {
late List<Ingredient> ingredients;
final searchController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
searchController.dispose();
super.dispose();
}
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
setState(() {});
}
Widget _searchBar(List<Ingredient> ingredients) {
return Row(
children: <Widget>[
IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.restaurant),
onPressed: null,
),
Expanded(
child: TextField(
controller: searchController,
onChanged: (newValue) {
updateResults(newValue);
},
cursorColor: Colors.black,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
hintText: "Search..."),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(
Icons.search,
color: Color(0xff9ccc65),
),
onPressed: () {
updateResults(searchController.text);
},
),
),
],
);
}
#override
void initState() {
this.ingredients = Ingredient.getDummyIngredients();
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(children: [
Expanded(flex: 1, child: _searchBar(this.ingredients)),
Expanded(flex: 4, child: IngredientsGrid(this.ingredients))
]),
);
}
}
class IngredientsGrid extends StatelessWidget {
List<Ingredient> ingredients;
IngredientsGrid(this.ingredients);
List<Widget> _buildIngredients() {
return this.ingredients.map((ing) => SingleIngredientIcon(ing)).toList();
}
// const IngredientsGrid({
// Key? key,
// }) : super(key: key);
#override
Widget build(BuildContext context) {
this.ingredients.forEach((ing) => print(ing.name! + ","));
return ListView(
children: <Widget>[
GridView.count(
crossAxisCount: 4,
// physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
shrinkWrap: true,
// You won't see infinite size error
children: _buildIngredients()),
// ...... other list children.
],
);
}
}
Moreover, I keep getting this Warning :
"Changing the content within the composing region may cause the input method to behave strangely, and is therefore discouraged. See https://github.com/flutter/flutter/issues/78827 for more details".
Visiting the linked GitHub page wasn't helpful
The problem is that while you are correctly filtering the list but your TextController is not getting assigned any value.
So, no value is getting assigned to your TextField as the initial value and hence the list again filters to have the entire list.
To solve this just assign the TextController the newValue like this.
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
// change
searchController = TextEditingController.fromValue(
TextEditingValue(
text: newValue,
),
);
setState(() {});
}
If it throws an error then remove final from the variable declaration, like this :
var searchController = TextEditingController();

How to compare value within 2 TextFormField

I wish to validate if the values within 2 TextFormFields matches.
I could validate them individually.
But how could I capture both those values to validate by comparing?
import 'package:flutter/material.dart';
class RegisterForm extends StatefulWidget {
#override
_RegisterFormState createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20),
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'Confirm Password'
),
validator: (value) {
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
RaisedButton(
onPressed: (){
if (_formKey.currentState.validate()) {
print('ok');
} else {
print('not ok');
}
},
),
],
),
)
],
),
);
}
}
One possible solution as follows.
I could store them as values within _RegisterFormState and retrieve them within the validate blocks. But is there a cleaner way to achieve this?
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
String password;
String confirmPassword;
.....
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
setState(() {
password = value;
});
performValidation(password, confirmPassword); // some custom validation method
return 'is empty';
}
return value;
},
),
.....
}
P.S: If there would be a better way to do it via a state management tool, I am using Provider. Not looking for Bloc solutions.
There are few steps I have performed to achieve this. You can refer that to achieve your goal.
Create a stateful widget and return a InputBox from there
add a property named as callback and set it's datatype as
ValueSetter callback;
assign this callback to onChanged Event of the input box
onChanged: (text) {
widget.callback(text);
},
Use your custom widget in the class where you want to use the input box
while using your widget pass a callback to it
InputWithLabel(
callback: (value) {
password = value;
},
),
InputWithLabel(
callback: (value) {
confirmPassword = value;
},
),
and at last, we have to compare those values,
you can bind a key to your form add use it in saved event of it. You can wrap it in Button's callback
if (InputValidation.validatePasswordandConfirm(password, cpassword)) {
// your after form code
}
To have it a real time comparison mark one of the input's callback in setState(){} and create a new property in your Custom Text Widget named as compareTxt;
and on validator check for compare text and return the error message
validator: (text) {
if (widget.comaparetext != text) {
return 'Password does not match';
}
I had the same problem but I was using custom TextFormField widgets.
So this is how I achieved it:
Problem:
I had two TextFormField widgets in a Form and I wanted to compare the values
of those two TextFormWidgets, to check whether the value of one filed is greater
then the other.
Solution:
I defined two controllers globally, because the TextFormWidgets were in a separate class.
final _startingRollNumberController = TextEditingController();
final _endingRollNumberController = TextEditingController();
Passed these controllers to the instances of TextFormField in the Form class.
CustomTextField('Starting R#', _startingRollNumberController),
CustomTextField('Ending R#', _endingRollNumberController),
And validated them in the TextFormField class.
class CustomTextField extends StatelessWidget {
// *===== CustomTextField class =====* //
CustomTextField(this._hintText, this._controller);
final String _hintText;
final _controller;
#override
Widget build(BuildContext context) {
return TextFormField(
validator: (value) {
final _startingRollNumber = _startingRollNumberController.text;
final _endingRollNumber = _endingRollNumberController.text;
// Starting roll-number needs to be smaller then the ending roll-number.
if (!_startingRollNumber.isEmpty && !_endingRollNumber.isEmpty) {
if (int.parse(_startingRollNumber) >= int.parse(_endingRollNumber)) {
return 'starting roll number must be smaller.';
}
}
},
);
}
}
This may not be the best approach but this is how I achieved it. There is
also, this flutter_form_builder package which can be used to achieve this easily using the form key.
A related question: Flutter: Best way to get all values in a form