Flutter: Multiple widgets used the same GlobalKey or Duplicate GlobalKeys - flutter

I am trying to create a dynamic form and using TextFormField for validation purpose.
Below is the code that is giving error Multiple widgets used the same GlobalKey or Duplicate Global key.
I am not sure how can i fix this or how can i make Dynamic Form clean as per standard.
import 'package:flutter/material.dart';
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String person;
String age;
String job;
var nameTECs = <TextEditingController>[];
var ageTECs = <TextEditingController>[];
var jobTECs = <TextEditingController>[];
var cards = <Card>[];
var nameController = TextEditingController();
var ageController = TextEditingController();
var jobController = TextEditingController();
#override
void initState() {
super.initState();
cards.add(createCard());
}
Card createCard() {
nameTECs.add(nameController);
ageTECs.add(ageController);
jobTECs.add(jobController);
return Card(
child:new Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Person ${cards.length + 1}'),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: nameController,
decoration: InputDecoration(labelText: 'Full Name'),
validator: validatetext,
onSaved: (String val) {
person = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: ageController,
decoration: InputDecoration(labelText: 'Age'),
validator: validatetext,
onSaved: (String val) {
age = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: jobController,
decoration: InputDecoration(labelText: 'Study/ job'),
validator: validatetext,
onSaved: (String val) {
job = val;
},
),
],
),
),
);
}
void _validateInputs() {
print('button');
if (_formKey.currentState.validate()) {
// If all data are correct then save data to out variables
_formKey.currentState.save();
_onDone();
}
}
_onDone() {
List<PersonEntry> entries = [];
for (int i = 0; i < cards.length; i++) {
var name = nameTECs[i].text;
var age = ageTECs[i].text;
var job = jobTECs[i].text;
entries.add(PersonEntry(name, age, job));
}
Navigator.pop(context, entries);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: cards.length,
itemBuilder: (BuildContext context, int index) {
return cards[index];
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RaisedButton(
child: Text('Add new'),
onPressed: () => setState(() => cards.add(createCard())),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RaisedButton(
child: Text('Remove last'),
onPressed: () => setState(() => cards.removeLast()),
),
)
],
),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.save), onPressed: _validateInputs),
);
}
}
class PersonEntry {
final String name;
final String age;
final String studyJob;
PersonEntry(this.name, this.age, this.studyJob);
#override
String toString() {
return 'Person: name= $name, age= $age, study job= $studyJob';
}
}
String validatetext(String value) {
if (value.length < 5)
return 'More than 5 char is required';
else
return null;
}
In case someone wants full error.
The following assertion was thrown while finalizing the widget tree:
Multiple widgets used the same GlobalKey.
The key [LabeledGlobalKey<FormState>#89788] was used by multiple widgets. The parents of those widgets were:
- Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#65de2 relayoutBoundary=up10)
- Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#f4085 relayoutBoundary=up10)
A GlobalKey can only be specified on one widget at a time in the widget tree.
When the exception was thrown, this was the stack
#0 GlobalKey._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure>.<anonymous closure>
package:flutter/…/widgets/framework.dart:246
#1 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:379:8)
#2 GlobalKey._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure>
package:flutter/…/widgets/framework.dart:193
#3 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:379:8)
#4 GlobalKey._debugVerifyGlobalKeyReservation.<anonymous closure>

The issue is You're using the same key _formKey when for all your forms. You can create a List of _formKeys that contains Globalkey<FormState> and key adding or removing to it based on the length of your cards.
I added a demo using your code as an example:
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
List<GlobalKey<FormState>> _formKeys = [
GlobalKey<FormState>()
]; // create a list of form keys
String person;
String age;
String job;
var nameTECs = <TextEditingController>[];
var ageTECs = <TextEditingController>[];
var jobTECs = <TextEditingController>[];
var cards = <Card>[];
var nameController = TextEditingController();
var ageController = TextEditingController();
var jobController = TextEditingController();
#override
void initState() {
super.initState();
cards.add(createCard());
}
Card createCard() {
nameTECs.add(nameController);
ageTECs.add(ageController);
jobTECs.add(jobController);
return Card(
child: new Form(
key: _formKeys[_formKeys.length-1], // acess each form key here
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Person ${cards.length + 1}'),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: nameController,
decoration: InputDecoration(labelText: 'Full Name'),
validator: validatetext,
onSaved: (String val) {
person = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: ageController,
decoration: InputDecoration(labelText: 'Age'),
validator: validatetext,
onSaved: (String val) {
age = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: jobController,
decoration: InputDecoration(labelText: 'Study/ job'),
validator: validatetext,
onSaved: (String val) {
job = val;
},
),
],
),
),
);
}
void _validateInputs() {
print('button');
for (int i = 0; i < _formKeys.length; i++) { // validate the form keys here
if (_formKeys[i].currentState.validate()) {
// validate each form
// If all data are correct then save data to out variables
_formKeys[i].currentState.save(); // dave each form
_onDone();
}
}
}
_onDone() {
List<PersonEntry> entries = [];
for (int i = 0; i < cards.length; i++) {
var name = nameTECs[i].text;
var age = ageTECs[i].text;
var job = jobTECs[i].text;
entries.add(PersonEntry(name, age, job));
}
Navigator.pop(context, entries);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: cards.length,
itemBuilder: (BuildContext context, int index) {
return cards[index];
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RaisedButton(
child: Text('Add new'),
onPressed: () => setState(
() {
_formKeys.add(GlobalKey<FormState>()); // add a new form key
cards.add(createCard());
},
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RaisedButton(
child: Text('Remove last'),
onPressed: () => setState(() {
cards.removeLast();
_formKeys.removeLast(); // remove the last form key
}),
),
)
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.save), onPressed: _validateInputs),
);
}
}
class PersonEntry {
final String name;
final String age;
final String studyJob;
PersonEntry(this.name, this.age, this.studyJob);
#override
String toString() {
return 'Person: name= $name, age= $age, study job= $studyJob';
}
}
String validatetext(String value) {
if (value.length < 5)
return 'More than 5 char is required';
else
return null;
}
RESULT:
NOTE: The answer is mainly focused on solving the issue of the GlobalKey, if you type in a Form it updates value in every Form because you are using the same controllers for the Forms, you can fix it by also creating a List of Controllers for your TextFormFields.

You're using the same key _formKey when creating a card and adding it to the list card, you should create a global key for each card as a list of the same size of cards, so every time you add/remove a card you do the same to the list of global key

Related

How to search/filter from a list from an API?

I have managed to load a list of cryptocurrencies from an API. This is done via the ListView.builder.
Subsequently, how does one perform a search/filter in order to select an item from the list?
By scrolling towards the end to see the last code, I have shown the code that I presumed would be able to do the job of 'search'. But I am unsure where to place this code.
Image below shows current crypto list loaded from API:
The code for the above screen is as follows:
class AddCryptoAssetScreen extends StatefulWidget {
#override
_AddCryptoAssetScreenState createState() => _AddCryptoAssetScreenState();
}
class _AddCryptoAssetScreenState extends State<AddCryptoAssetScreen> {
Future<List<Asset>> fetchCoin() async {
assetList = [];
final response = await http.get(Uri.parse(
'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false'));
if (response.statusCode == 200) {
List<dynamic> values = [];
values = json.decode(response.body);
if (values.length > 0) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
Map<String, dynamic> map = values[i];
assetList.add(Asset.fromJson(map));
}
}
setState(() {
assetList;
});
}
return assetList;
} else {
throw Exception('Failed to load coins');
}
}
#override
void initState() {
fetchCoin();
Timer.periodic(Duration(seconds: 1), (timer) => fetchCoin());
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.pop(context),
child: DraggableScrollableSheet(
builder: (_, controller) => Container(
decoration: BoxDecoration(),
clipBehavior: Clip.antiAlias,
child: Scaffold(
appBar: AppBar(),
body: Column(
children: [
Container(
margin: const EdgeInsets.fromLTRB(),
child: TextField(
keyboardType: TextInputType.text,
textAlign: TextAlign.start,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: 'Search',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.only()),
),
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: assetList.length,
itemBuilder: (context, index) {
return AssetCryptoCard(
name: assetList[index].name,
image: assetList[index].image,
);
},
),
),
],
),
),
),
),
);
}
}
The Asset class is as follows. To derive assetList.
class Asset {
String name;
String image;
num currentPrice;
num priceChange24h;
num priceChangePercentage24h;
String symbol;
Asset({
required this.name,
required this.image,
required this.currentPrice,
required this.priceChange24h,
required this.priceChangePercentage24h,
required this.symbol,
});
factory Asset.fromJson(Map<String, dynamic> json) {
return Asset(
name: json['name'],
symbol: json['symbol'],
image: json['image'],
currentPrice: json['current_price'],
priceChange24h: json['price_change_24h'],
priceChangePercentage24h: json['price_change_percentage_24h'],
);
}
}
List<Asset> assetList = [];
The AssetCryptoCard class is as follows.
class AssetCryptoCard extends StatelessWidget {
AssetCryptoCard({
required this.name,
required this.image,
});
final String name;
final String image;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => EditAssetScreen(),
);
},
child: Container(
padding: EdgeInsets.only(),
child: Column(
children: [
Row(
children: [
Container(child: Image.network(image)),
SizedBox(),
Text(name),
Spacer(),
Icon(Icons.arrow_forward_ios_rounded),
],
),
Container(),
],
),
),
);
}
}
I have written the code below but unsure where is the right place to put it. Also, presumably this is the right code to do a search/filter on the list.
List<Map<String, dynamic>> foundAssetList = [];
#override
initState() {
foundAssetList = assetList;
super.initState();
}
void _runFilter(String enteredKeyword) {
List<Map<String, dynamic>> results = [];
if (enteredKeyword.isEmpty) {
results = assetList;
} else {
results = assetList
.where((user) =>
user["name"].toLowerCase().contains(
enteredKeyword.toLowerCase().toList();
}
setState(() {
foundAssetList = results;
});
}
Any help would be much appreciated.
You're missing a few things:
You need a state that keeps track of the text in the TextField - you can do this by adding a state variable and use onChanged of the TextField to update the variable
Write a function that returns a list of the cryptocurrencies based on the keyword: You can create a function that returns the list of all cryptocurrencies if the keyword is empty, else return a filtered list
Replace the assetList variable in the ListView.builder with the filteredList (should be the value of the function in step 2)
The result code should look like this (put this in _AddCryptoAssetScreenState:
String _keyword = "";
List<Asset> _getFilteredList() {
if (_keyword.isEmpty) {
return assetList;
}
return assetList
.where((user) =>
user.name.toLowerCase().contains(
_keyword.toLowerCase())).toList();
}
#override
Widget build(BuildContext context) {
final filteredList = _getFilteredList();
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.pop(context),
child: DraggableScrollableSheet(
builder: (_, controller) => Container(
decoration: BoxDecoration(),
clipBehavior: Clip.antiAlias,
child: Scaffold(
appBar: AppBar(),
body: Column(
children: [
Container(
margin: const EdgeInsets.fromLTRB(),
child: TextField(
keyboardType: TextInputType.text,
textAlign: TextAlign.start,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: 'Search',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.only()),
onChanged: (text) {
setState(() {
_keyword = text;
});
},
),
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: filteredList.length,
itemBuilder: (context, index) {
return AssetCryptoCard(
name: filteredList[index].name,
image: filteredList[index].image,
);
},
),
),
],
),
),
),
),
);
}
Let me know if this works.
Use the textfoemfield onChanged property
TextFormField(
OnChanged: (value){
_runFilter(value);
}
);
Or
For API request use future feature in textfield_search package
please review this package can provide solution with different approach
I hope it works for you.

The setter 'firstName=' was called on null. Receiver: null Tried calling: firstName="hfjfhd" in flutter

Someone please help me, I am stuck on this error since last two days, what I am trying send data from input form using on clicking on a button to the lists, when clicked on a button, a form opens on the same page but when I input the data in form and clicks the submit button, nothing happens and on terminal it shows The setter 'firstName=' was called on null. Receiver: null Tried calling: firstName="hfjfhd".
this is my model.dart file
class Model { string firstName = ""; String lastName = ""; String email = ""; String password = ""; Model({this.firstName, this.lastName, this.email, this.password});}
and this is my main file on which i am opening the form and showing the result.
import 'package:flutter/material.dart';
import 'model.dart';
import 'package:dummy_project/components/save_data_from_input_into_object/form.dart';
import 'package:dummy_project/components/save_data_from_input_into_object/result.dart';
class DialogForm extends StatefulWidget {
List<Model> models = <Model>[];
Model tempModel;
DialogForm();
#override
State<DialogForm> createState() => _ResultState();
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Model>('model', tempModel));
}
}
class _ResultState extends State<DialogForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return (Scaffold(
appBar: AppBar(title: Text('Successful')),
body: Container(
margin: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Flexible(
child: ListView.builder(
itemCount: widget.models.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.models[index].firstName),
subtitle: Text(widget.models[index].lastName),
trailing: Text(widget.models[index].email),
);
},
),
),
Align(
child: RaisedButton(
child: Text('Click Me!'),
onPressed: (){
showDialog(
context: context,
builder: (BuildContext context){
return AlertDialog(
content: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
right: -40,
top: -40,
child: InkResponse(
onTap: () {
Navigator.of(context).pop();
},
child: CircleAvatar(
child: Icon(Icons.close),
backgroundColor: Colors.red,
),
),
),
Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(8.0),
child: MyTextFormField(
hintText: 'First Name',
validator: (String value) {
if (value.isEmpty) {
return 'Enter your first name';
}
return null;
},
onSaved: (String value) {
widget.tempModel.firstName = value;
},
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: MyTextFormField(
hintText: 'Last Name',
validator: (String value) {
if (value.isEmpty) {
return 'Enter your last name';
}
return null;
},
onSaved: (String value) {
widget.tempModel.lastName = value;
},
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: MyTextFormField(
hintText: 'Enter email',
isEmail: true,
// validator: (String value) {
// if (!validator.isEmail(value)) {
// return 'Please enter a valid email';
// }
// return null;
// },
onSaved: (String value) {
widget.tempModel.email = value;
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text("Submit"),
onPressed: () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
widget.models.add(widget.tempModel);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DialogForm()));
}
},
),
)
],
)
),
],
),
);
}
);
}
),
alignment: Alignment.bottomRight,
),
],
),
),
)
);
}
}
class MyTextFormField extends StatelessWidget {
final String hintText;
final Function validator;
final Function onSaved;
final bool isPassword;
final bool isEmail;
MyTextFormField({
this.hintText,
this.validator,
this.onSaved,
this.isPassword = false,
this.isEmail = false,
});
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
decoration: InputDecoration(
hintText: hintText,
contentPadding: EdgeInsets.all(15.0),
border: InputBorder.none,
filled: true,
fillColor: Colors.grey[200],
),
obscureText: isPassword ? true : false,
validator: validator,
onSaved: onSaved,
keyboardType: isEmail ? TextInputType.emailAddress : TextInputType.text,
),
);
}
}```
Make it nullable : "?" here states that it can be null by default or by any operation.
Suggestions : use textEditingController to get input value, and pass it.
and use null safe check when showing texts from model, cause you defined it as null.
widget.models[index].firstName ?? "First Name"
above, "??" means if left statement is in any case is null, then right statement will get executed, so if you are getting "First Name" in your text field then its getting null from model.
class Model{
String? firstName;
String? lastName;
String? email;
String? password;
Model({
this.firstName,
this.lastName,
this.email,
this.password
});
}
Pl let me know if you have any doubts
Thanks
You don't initialise tempModel, so it will be null, that's why you get the error message for example at widget.tempModel.firstName.
Instead of Model tempModel; try:
Model tempModel = Model();
or
final tempModel = Model();

I am trying to create list of TextFormFields which takes numbers as inputs and I want to Sum all those numbers

I am trying to create list of TextFormFields which takes numbers as inputs and I want to Sum all those numbers. When I click on a button on app bar new textformfield appears and user enters value..validator is also working fine...But I am not able to do the Sum. When I used print in Onsaved method it displays all the entered values..If I use Controller, whatever the text we enter in formfield it is displaying same same in all the other textfields also..so controller is not working...I created TextFormField in different function and calling that function when button is pressed. I created another button to go to next screen at the same time to validate which works fine...
Below is the TextFormField code: Please help to Sum all the values entered in it:
child: TextFormField(
// controller: _childController,
decoration: InputDecoration(
hintText: 'Value $_count',
border: InputBorder.none,
contentPadding: EdgeInsets.only(top: 5, left: 20)),
keyboardType: TextInputType.number,
style: TextStyle(
color: Color.fromARGB(255, 0, 0, 0),
fontWeight: FontWeight.w400,
fontSize: 24,
),
validator: (String value) {
double sal = double.tryParse(value);
if (sal == null) {
return 'enter or delete row';
}
},
onSaved: (String value) {
// print(_childController.text);
// print(value);
_mVal = value;
double _mVal2 = double.tryParse(_mVal);
double _mVal3;
print(_mVal);
int k = 0;
_children.forEach((element) {
int y = int.tryParse(_mVal);
k=k+y;
print(k);
}
Here is a quick example of how you can achieve this:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Test(),
),
);
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
final _formKey = GlobalKey<FormState>();
List<TextEditingController> textFieldControllers = [];
int numberOfTextFields = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
addNewTextField();
},
),
body: Stack(
children: [
Form(
key: _formKey,
child: ListView.builder(
itemCount: numberOfTextFields,
itemBuilder: (BuildContext context, int index) {
return TextFormField(
validator: (String value) {
double sal = double.tryParse(value);
if (sal == null) {
return 'enter or delete row';
}
return null;
},
controller: textFieldControllers[index],
);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: () {
if (_formKey.currentState.validate()) {
showDialog(
context: context,
builder: (BuildContext context) {
return Center(
child: Material(
child: Container(
padding: EdgeInsets.all(10.0),
child: Text(
'The sum is ${textFieldControllers.fold(0, (previousValue, element) => previousValue + int.parse(element.value.text))}'),
),
),
);
});
}
},
child: Container(
padding: EdgeInsets.all(10.0),
color: Colors.redAccent,
child: Text('Tap to sum'),
),
),
),
],
),
);
}
void addNewTextField() {
textFieldControllers.add(TextEditingController());
numberOfTextFields++;
setState(() {});
}
#override
void dispose() {
textFieldControllers.forEach((textFieldController) => textFieldController.dispose());
super.dispose();
}
}
You can expand on this idea to remove textField if needed. Just don't forget to dispose your textFields.
How does this work: Each time a TextField Widget is create, an associated TextEditingController is created and given to the TextField. When we want to sum, we just iterate on the TextEditingController list.

Create dynamic radio buttons in Flutter

I am trying to create a dynamic form which contains some textbox and radio button. I am using RadioListTile for the same.
In the below code you can see I am using var nameController = TextEditingController(); to get the value of textbox. I am not sure what can be used for RadioListTile.
I am also struggling to show Radio Button Dynamically. I have added full code in the below. How can I get the radio button working and get the value of the selected items, so they can be saved to the database?
class Price extends StatefulWidget {
#override
_PriceState createState() => _PriceState();
}
class FruitsList {
String name;
int index;
FruitsList({this.name, this.index});
}
class _PriceState extends State<Price> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _scaffoldKey = GlobalKey<ScaffoldState>();
int currentIndex = 0;
String person;
String age;
String job;
// Default Radio Button Item
String radioItem = 'Mango';
// Group Value for Radio Button.
int id = 1;
List<FruitsList> fList = [
FruitsList(
index: 1,
name: "Mango",
),
FruitsList(
index: 2,
name: "Banana",
),
FruitsList(
index: 3,
name: "Apple",
),
FruitsList(
index: 4,
name: "Cherry",
),
];
#override
void initState() {
super.initState();
cards.add(createCard());
}
var nameTECs = <TextEditingController>[];
var ageTECs = <TextEditingController>[];
var jobTECs = <TextEditingController>[];
--- Need to help to add Controller for Radio Button ---
var cards = <Card>[];
Card createCard() {
var nameController = TextEditingController();
var ageController = TextEditingController();
var jobController = TextEditingController();
nameTECs.add(nameController);
ageTECs.add(ageController);
jobTECs.add(jobController);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Service ${cards.length + 1}'),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: validatetext,
onSaved: (String val) {
person = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: ageController,
decoration: InputDecoration(labelText: 'age'),
validator: validatetext,
onSaved: (String val) {
age = val;
},
),
TextFormField(
style: TextStyle(color: Colors.blue),
controller: jobController,
decoration: InputDecoration(labelText: 'Job'),
validator: validatetext,
onSaved: (String val) {
job = val;
},
),
//Expanded(
// child: Container(
// height: 350.0,
// child:
Row(
children:
fList.map((data) => RadioListTile(
title: Text("${data.name}"),
groupValue: id,
value: data.index,
onChanged: (val) {
setState(() {
radioItem = data.name ;
id = data.index;
});
},
)).toList(),
),
//)),
/* CheckboxListTile(
title: Text("title text"),
value: checkedValue,
onChanged: (newValue) {
setState(() {
checkedValue = newValue;
});
},
//onChanged: (newValue) { ... },
controlAffinity: ListTileControlAffinity.leading, // <-- leading Checkbox
), */
SizedBox(height: 10),
],
),
// ),
);
}
void _validateInputs() {
print('button');
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_onDone();
} else {
}
}
_onDone() {
updateProfile();
List<PersonEntry> entries = [];
for (int i = 0; i < cards.length; i++) {
var name = nameTECs[i].text;
var age = ageTECs[i].text;
var job = jobTECs[i].text;
entries.add(PersonEntry(name, age, job));
}
}
///////// Save to DB ////////////////////
Future updateProfile() async{
try{
for (int i = 0; i < cards.length; i++) {
var name = nameTECs[i].text;
var age = ageTECs[i].text;
var job = jobTECs[i].text;
Map<String, dynamic> body = {'name': name, 'age': age, 'job' : job };
print(body);
nameTECs[i].clear();
//if(rang == true){
Response response =
await Dio().post("http://192.168.1.102:8080/adddetails.php", data: body);
print(response.statusCode);
if(response.statusCode == 404){
print('404');
}
if(response.statusCode == 200){
nameTECs[i].clear();
}
}
} catch (e) {
print("Exception Caught: $e");
}
}
///////////////////////////////
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: myAppBar(),
endDrawer: myDrawer(),
body: Column(
children: <Widget>[
Expanded(
child:new Form(
key: _formKey,
child: ListView.builder(
itemCount: cards.length,
itemBuilder: (BuildContext context, int index) {
return cards[index];
},
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 2.0),
color: Colors.grey,
child:Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Container(
Padding(
padding: const EdgeInsets.all(16.0),
child: FloatingActionButton(
heroTag: "btn1",
child: Icon(Icons.add),
onPressed: () => setState(() => cards.add(createCard())),
backgroundColor: Colors.green,
)
/*RaisedButton(
child: Text('Add new'),
onPressed: () => setState(() => cards.add(createCard())),
),*/
),
Padding(
padding: const EdgeInsets.all(16.0),
child: FloatingActionButton(
heroTag: "btn2",
child: Icon(Icons.remove), onPressed: () => setState(() => cards.removeLast()),
backgroundColor: Colors.red,
)
),
Padding(
padding: const EdgeInsets.all(16.0),
child: FloatingActionButton(
heroTag: "btn3",
child: Icon(Icons.save), onPressed: _validateInputs),
)
],
),
),
],
),
);
}
);
}
}
class PersonEntry {
final String name;
final String age;
final String studyJob;
PersonEntry(this.name, this.age, this.studyJob);
#override
String toString() {
return 'Person: name= $name, age= $age, study job= $studyJob';
}
}
Size get preferredSize => Size.fromHeight(kToolbarHeight);
String validatetext(String value) {
if (value.length < 5)
return 'More than 5 char is required';
else
return null;
}
Update
I want to show Radio buttons that user can select and once user submit the form I can get those value for http request. As you can I have added options to add or remove cards. So, these radio buttons will also generated.
Create field int _selectedRadioIndex
and change code
fList.map((data) => RadioListTile(
title: Text("${data.name}"),
groupValue: id,
value: data.index,
onChanged: (val) {
setState(() {
radioItem = data.name ;
id = data.index;
});
},
)).toList(),
to
fList.map((data) => RadioListTile(
title: Text("${data.name}"),
groupValue: id,
value: data.index,
onChanged: (val) {
setState(() {
radioItem = data.name ;
id = data.index;
_selectedRadioIndex = val;
});
},
)).toList(),
then in code just get it fList.firstWhere((element) => element.index == _selectedRadioIndex)

TextFromField is losing value after sate changed

TextFromField is losing its value when the state change.
Here is the full code https://github.com/imSaharukh/cgpa_final.git
How can I fix that?
Check this GIF
The problem is how you create your TextEditingControllers. Everytime the build method is called new TextEditingControllers are created.
What you want to do is create 3 TextEditingController variables at the top inside _MyHomePageState class. (Also no need to use the new keyword in dart).
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
TextEditingController nameController = TextEditingController();
TextEditingController cgpaController = TextEditingController();
TextEditingController crController = TextEditingController();
and pass these to your CustomCard
child: CustomCard(
key: UniqueKey(),
index: index,
cgpa: cgpa,
namecontroller: nameController,
cgpacontroller: cgpaController,
crcontroller: crController),
Hope this helps
EDIT:
I don't know how to create a pull request but I made some changes for you and tested it on an iOS sim.
What i did:
Renamed Details to Course
Converted CusomCard into an statefull widget
Only a Course object is now passed to CustomCard
The dismissable now gets a key based on the course.
Moved the controllers to CustomCard
Modified some code in CGPA to make it all work
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Column(
children: [
Expanded(
child: Consumer<CGPA>(builder: (context, cgpa, _) {
return Form(
key: _formKey,
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: cgpa.courses.length,
itemBuilder: (BuildContext context, int index) {
return Dismissible(
key: Key(cgpa.getKeyValue(index)),
onDismissed: (direction) {
cgpa.remove(index);
print(cgpa.courses.length);
},
child: CustomCard(
course: cgpa.getCourse(index),
),
);
},
),
);
}),
),
],
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(15.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Provider.of<CGPA>(context, listen: false).add();
// print(cgpa.details.length);
// cgpa.details[indexs] = Details();
},
),
),
),
],
),
),
floatingActionButton: OutlineButton(
onPressed: () {
// for (var item in cgpa.details) {
// print(item.credit);
// }
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text("calculate"),
),
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(30.0),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
CustomCard
class CustomCard extends StatefulWidget {
CustomCard({#required this.course});
final Course course;
#override
_CustomCardState createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
TextEditingController nameController;
TextEditingController cgpaController;
TextEditingController crController;
#override
void initState() {
super.initState();
nameController = TextEditingController(text: widget.course.name);
cgpaController = TextEditingController(
text: widget.course.gpa == null ? "" : widget.course.gpa.toString());
crController = TextEditingController(
text: widget.course.credit == null
? ""
: widget.course.credit.toString());
}
#override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 3,
child: TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: "COURSE NAME"),
onChanged: (value) {
widget.course.name = value;
},
),
),
SizedBox(
width: 10,
),
Expanded(
child: TextFormField(
controller: cgpaController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: "GPA"),
onChanged: (value) {
//print(value);
widget.course.gpa = double.parse(value);
},
validator: (value) {
if (double.parse(value) > 4 && double.parse(value) < 0) {
return 'can\'t more then 4';
}
return null;
},
),
),
SizedBox(
width: 10,
),
Expanded(
child: TextFormField(
controller: crController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: "CREDIT"),
onChanged: (value) {
widget.course.credit = double.parse(value);
},
validator: (value) {
if (value.isEmpty) {
return 'can\'t be empty';
}
return null;
},
),
),
],
),
),
);
}
}
CGPA
class CGPA with ChangeNotifier {
Map<int, Course> courses = new Map();
var index = 0;
add() {
courses[index] = Course();
index++;
notifyListeners();
}
remove(int listIndex) {
courses.remove(courses.keys.toList()[listIndex]);
notifyListeners();
}
String getKeyValue(int listIndex) => courses.keys.toList()[listIndex].toString();
Course getCourse(int listIndex) => courses.values.toList()[listIndex];
}
class Course {
Course({this.credit, this.gpa, this.name});
String name;
double credit;
double gpa;
#override
String toString() {
return 'Course{name: $name, credit: $credit, gpa: $gpa}';
}
}