TextFromField is losing value after sate changed - flutter

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}';
}
}

Related

Save input values between widget rebuilds with Bloc Flutter

I have a form builded with Bloc package.
There are two options with textfields in it.
Switching between option i've made also with bloc and Visibility widget.
When I choose an option widget rebuilds, name and price values deletes.
What is the best way to save this values between choosing options?
Here is my Bloc code
class FormBloc extends Bloc<FormEvent, MyFormState> {
FormBloc() : super(MyFormState()) {
on<RadioButtonFormEvent>(_setRadioButtonState);
}
void _setRadioButtonState(
RadioButtonFormEvent event, Emitter<MyFormState> emit) async {
emit(RadioButtonFormState(
buttonIndex: event.buttonIndex,
buttonName: event.buttonName,
));
}
}
class MyFormState {}
class RadioButtonFormState extends MyFormState {
final int buttonIndex;
final String buttonName;
RadioButtonFormState({
required this.buttonIndex,
required this.buttonName,
});
}
abstract class FormEvent extends Equatable {}
class RadioButtonFormEvent extends FormEvent {
final int buttonIndex;
final String buttonName;
RadioButtonFormEvent({
required this.buttonIndex,
required this.buttonName,
});
#override
List<Object?> get props => [buttonIndex, buttonName,];
}
Here is Form code
class FormInput extends StatelessWidget {
const FormInput({super.key});
#override
Widget build(BuildContext context) {
final formBlocWatcher = context.watch<FormBloc>().state;
final nameController = TextEditingController();
final priceController = TextEditingController();
final formOneController = TextEditingController();
final formTwoController = TextEditingController();
final formThreeController = TextEditingController();
String formOptionController = '';
bool optionOneIsActive = true;
bool optionTwoIsActive = false;
if (formBlocWatcher is RadioButtonFormState) {
switch (formBlocWatcher.buttonIndex) {
case 0:
formOptionController = formBlocWatcher.buttonName;
break;
case 1:
optionTwoIsActive = true;
optionOneIsActive = false;
formOptionController = formBlocWatcher.buttonName;
break;
}
}
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 15,
left: 15,
right: 15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(hintText: 'Name'),
),
const SizedBox(height: 10),
TextField(
controller: priceController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Price'),
),
const SizedBox(height: 15),
OptionsWidget(),
Visibility(
visible: optionOneIsActive,
child: TextField(
controller: formOneController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form one'),
)),
Visibility(
visible: optionTwoIsActive,
child: Column(
children: [
TextField(
controller: formTwoController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form two'),
),
TextField(
controller: formThreeController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form three'),
),
],
)),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
BlocProvider.of<ProductsListBloc>(context).add(
AddProductEvent(
productName: nameController.text,
productPrice: int.parse(priceController.text),
productDescOne: formOneController.text,
productDescTwo: formTwoController.text,
productDescThree: formThreeController.text,
formOption: formOptionController,
),
);
},
child: Text('Create New'),
),
],
),
);
}
}
class OptionsWidget extends StatelessWidget {
OptionsWidget({super.key});
int value = 0;
Widget CustomRadioButton(String text, int index, BuildContext context) {
final formBloc = BlocProvider.of<FormBloc>(context);
final blocWatch = context.watch<FormBloc>().state;
if (blocWatch is RadioButtonFormState) {
value = blocWatch.buttonIndex;
}
return OutlinedButton(
onPressed: () {
formBloc.add(RadioButtonFormEvent(
buttonIndex: index,
buttonName: text,
));
},
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: BorderSide(color: (value == index) ? Colors.blue : Colors.grey),
),
child: Text(
text,
style: TextStyle(
color: (value == index) ? Colors.blue : Colors.grey,
),
));
}
#override
Widget build(BuildContext context) {
return Row(
children: [
CustomRadioButton("option one", 0, context),
const SizedBox(width: 15),
CustomRadioButton("option two", 1, context),
],
);
}
}
Your FormInput class should be extends from StatefulWidget, not StatelessWidget.
After this change, you should remove all TextEditingController assignments from the build() method and move them into initState().

How to pass data from child widget to parent widget using provider or valuenotifier in flutter

Here is my parent widget.
class AddUserextends StatefulWidget {
final ScrollController controller;
AddUser(this.controller);
#override
_AddUser createState() =>
_AddUser();
}
class _AddUserToManagePropertyState extends State<AddUserToManageProperty> {
late TextEditingController _firstNameCtrl;
late TextEditingController _lastNameCtrl;
late TextEditingController _phoneNoCtrl;
late ValueNotifier<num?> _category;
final _scaffoldKey = GlobalKey<ScaffoldState>();
final _formKey = GlobalKey<FormState>();
final ValueNotifier<bool> _formStateEmitter = ValueNotifier(false);
final ValueNotifier<bool> isSelected = ValueNotifier(false);
#override
void initState() {
super.initState();
_firstNameCtrl = TextEditingController(text: '');
_lastNameCtrl = TextEditingController(text: '');
_phoneNoCtrl = TextEditingController(text: '');
_firstNameCtrl.addListener(() {
_formStateEmitter.value = _fieldsStatus();
});
_lastNameCtrl.addListener(() {
_formStateEmitter.value = _fieldsStatus();
});
_phoneNoCtrl.addListener(() {
_formStateEmitter.value = _fieldsStatus();
});
}
bool _fieldsStatus() {
return HwValidators.required(_firstNameCtrl.text) == null &&
HwValidators.required(_lastNameCtrl.text) == null &&
HwValidators.required(_phoneNoCtrl.text) == null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: HwAppBar(
title: 'Add team member',
),
body: Form(
key: _formKey,
child: FormWidget(
maintainSafeArea: false,
showBackBtn: false,
fields: [
HwTextField(
label: 'FIRST NAME',
controller: _firstNameCtrl,
validator: HwValidators.nameValidator,
keyboardType: TextInputType.text,
),
HwSizedBox(height: 4),
HwTextField(
label: 'LAST NAME',
controller: _lastNameCtrl,
validator: HwValidators.nameValidator,
keyboardType: TextInputType.text,
),
HwSizedBox(height: 4),
HwTextField(
label: 'PHONE NO',
controller: _phoneNoCtrl,
validator: HwValidators.phoneValidator,
keyboardType: TextInputType.phone,
),
HwSizedBox(height: 4),
HwSizedBox(height: 4),
HwText('ACCESS LEVEL'),
UserAccessListTiles(),
HwSizedBox(
height: 4,
),
],
),
),
);
}
}
The second last widget is UserAccessListTiles which is a widget that allows user to choose from two set of widgets like this:
class UserAccessListTiles extends StatelessWidget {
List _userAccessListTile = [
SelectableTile(
title: 'Access to all stories',
leading: SvgPicture.asset(HwSvgs.fullAccess),
trailing: null,
),
SelectableTile(
title: 'Custom access',
leading: SvgPicture.asset(HwSvgs.customAccess),
trailing: null),
];
final ValueNotifier<int> _selected = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _selected,
builder: (context, int value, child) {
return Column(
children: [
Container(
child: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 2,
itemBuilder: (context, index) {
return Column(
children: [
HwSizedBox(
height: 3,
),
InkWell(
onTap: () {
_selected.value = index;
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: value == index ? 2 : 1,
color: value == index
? HwColors.green
: HwColors.divider,
),
borderRadius: BorderRadius.circular(8.0)),
child: SelectableTile(
title: _userAccessListTile[index].title,
leading: _userAccessListTile[index].leading,
trailing: value == index
? SvgPicture.asset(HwSvgs.greenCheck)
: null,
),
),
),
],
);
},
),
),
_selected.value == 0 ? UserAccessListTilesRadio() : CustomAccess(),
],
);
});
}
}
And finally both these widgets UserAccessListTilesRadio() and CustomAccess() have a set of RadioListTile that user can choose from and I want that chosen option to be available in the original parent widget as part of the Form.
How can I do it, please help.
I would recommend you to use state management i.e Bloc or Provider
e.g Provider
Make a model that extends ChangeNotifierProvider
class MyProviderModel extends ChangeNotifierProvider{
int yourChosenValue;
void updateChosenValue() {
// Your logic
notifyListeners();
}
}
Init Provider in your Parent Widget
Provider(
create: (_) => MyProviderModel(),
child: Consumer<MyProviderModel>(
builder: (_, a, child) {
return // Your Form
},
)
)
Update Your MyProviderModel in your child Widget
context.read<MyProviderModel>().updateChosenValue();
Init Provider in your parent Widget

Flutter: Multiple widgets used the same GlobalKey or Duplicate GlobalKeys

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

StatefulWidgets in ReorderableListView don't keep their state when reordering the list

Here I have a trimmed down page which creates a ReorderableListView, which has its body set to two RecipeStepWidgets with UniqueKeys set (I've also tried this with ValueKey and ObjectKey)
import 'package:flutter/material.dart';
import 'consts.dart';
import 'recipeStep.dart';
import 'recipeStepWidget.dart';
class NewRecipePage extends StatefulWidget {
#override
_NewRecipePageState createState() => _NewRecipePageState();
}
class _NewRecipePageState extends State<NewRecipePage> {
final TextEditingController _nameController = TextEditingController();
#override
Widget build(BuildContext context) {
List<RecipeStep> recipeSteps = [];
List<RecipeStepWidget> stepWidgets = [
RecipeStepWidget(key: UniqueKey()),
RecipeStepWidget(key: UniqueKey())
];
void _onReorder(int oldIndex, int newIndex) {
setState(
() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final RecipeStepWidget item = stepWidgets.removeAt(oldIndex);
stepWidgets.insert(newIndex, item);
},
);
}
return Scaffold(
appBar: AppBar(title: Text("New Recipe")),
body: Column(
children: <Widget>[
Expanded(
child: ReorderableListView(
header: Text("Steps"),
onReorder: _onReorder,
children: stepWidgets,
),
),
],
),
);
}
}
The RecipeStepWidget class is (ignoring includes):
class RecipeStepWidget extends StatefulWidget {
RecipeStepWidget({#required Key key}) : super(key: key);
_RecipeStepWidgetState createState() => _RecipeStepWidgetState();
}
class _RecipeStepWidgetState extends State<RecipeStepWidget> {
TextEditingController _controller = TextEditingController();
TextEditingController _durationLowController = TextEditingController();
TextEditingController _durationHighController = TextEditingController();
bool concurrent = false;
RecipeStep toRecipeStep() {
return RecipeStep(
text: _controller.text,
);
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
controller: _controller,
decoration: InputDecoration(hintText: "Step text"),
),
Row(
children: <Widget>[
Text("Duration min: "),
Container(
width: 40.0,
//height: 100.0,
child: TextField(
controller: _durationLowController,
keyboardType: TextInputType.number,
decoration: InputDecoration(hintText: "0"),
onChanged: (String val) {
if (_durationHighController.text.isEmpty ||
int.parse(val) >
int.parse(_durationHighController.text)) {
_durationHighController.text = val;
}
},
),
),
Text(" max: "),
Container(
width: 40.0,
//height: 100.0,
child: TextField(
controller: _durationHighController,
keyboardType: TextInputType.number,
decoration: InputDecoration(hintText: "0"),
),
),
],
),
Row(
children: <Widget>[
Text("Start concurrently with previous step"),
Checkbox(
value: concurrent,
onChanged: (bool val) => {
setState(() {
concurrent = val;
})
}),
],
),
],
);
}
}
When I edit the textfields or checkboxes in the RecipeStateWidgets and then reorder them within the list by clicking+dragging them, the widgets get reset to their default state.
Does anyone have any ideas why this is happening? I thought that all I had to do in order to get the ReorderableListView to work as intended was to set a key on each of its children. Is that not correct?
Thanks!
I think you can use AutomaticKeepAliveClientMixin like so:
class _RecipeStepWidgetState extends State<RecipeStepWidget> with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
Giving the list item a ValueKey seems to fix the problem for me. Hopefully helps in your situation as well.
e.g.
List<YourModel> _items = [];
Widget _itemsListWidget() {
return ReorderableListView(
onReorder: (oldIndex, newIndex) {
//
},
children: [
for (int index = 0; index < _items.length; index += 1)
Text(
_items[index],
key: ValueKey(_items[index].id), // <--- This is what you need to add
),
],
);
}

how to remove widget based on it's index in flutter

I've question about how to close the appropriate widget based on the close button index. here in this image, you can see the output so here I am adding some widget using add button which located inside app bar now i've added close button inside the container when the user pressed the close button it will remove that container.
Here is the image of output :
Here is the code i've tried
class BspUnlicensedSignupPage extends StatefulWidget {
static const String routeName = "/bspUnlicensedSignup";
final BspSignupCommonModel bspSignupCommonModel;
BspUnlicensedSignupPage({
Key key,
#required this.bspSignupCommonModel,
}) : super(key: key);
#override
_BspUnlicensedSignupPageState createState() =>
_BspUnlicensedSignupPageState();
}
class _BspUnlicensedSignupPageState extends State<BspUnlicensedSignupPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List<Object> images = List<Object>();
Future<File> _imageFile;
bool autovalidate = false;
bool informationislegitimate = false;
DateTime expirydate1 = DateTime.now();
DateTime expirydate2 = DateTime.now();
final format = DateFormat("yyyy-MM-dd");
final format2 = DateFormat("yyyy-MM-dd");
String type2 = 'Passport';
List<String> _type = <String>[
'',
'Passport',
'Driving License',
'Voter ID card',
'Ration Card',
'Aadhar',
'Other Id',
];
String type = 'Passport';
var _myWidgets = List<Widget>();
int _index = 3;
final Map<int, String> identification1Values = Map();
final Map<int, String> documentValues = Map();
final Map<int, DateTime> expiryDateValues = Map();
final Map<int, String> issuingAuthority = Map();
final Map<int, String> identificationPicturesValues = Map();
final List<TextEditingController> _documentControllers = List();
final List<TextEditingController> _issuingauthoritytype = List();
final List<TextEditingController> _expiryDate = List();
final List<TextEditingController> _issuingauthority = List();
final List<List<Object>> _identificationpictures = List();
#override
void initState() {
super.initState();
setState(() {
images.add("Add Image");
images.add("Add Image");
images.add("Add Image");
images.add("Add Image");
images.add("Add Image");
});
}
void _add() {
int keyValue = _index;
_myWidgets = List.from(_myWidgets)
..add(Column(
key: Key("$keyValue"),
children: <Widget>[
SizedBox(height: 10),
Container(
// padding: EdgeInsets.fromLTRB(18,5,18,18),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 15,
),
],
),
child: Column(
children: <Widget>[
Stack(
children: <Widget>[
Align(
alignment: Alignment.topRight,
child: GestureDetector(
child: Icon(Icons.close),
onTap: () {
print("CLose pressed");
_myWidgets.removeAt(_index);
},
),
),
SizedBox(
height: 10,
),
Column(
children: <Widget>[
SizedBox(
height: 20,
),
_buildidentificationtype1(keyValue),
_builddocumentnumber1(keyValue),
_builddate(keyValue),
_buildissuingauthority1(keyValue),
_buildidentificationpictures(keyValue),
],
),
],
)
],
),
)
],
));
setState(() => ++_index);
}
bool isClicked = false;
Widget _buildidentificationtype1(int keyValue) {
TextEditingController controller = TextEditingController();
_issuingauthoritytype.add(controller);
return FormBuilder(
autovalidate: autovalidate,
child: FormBuilderCustomField(
attribute: "Business type",
validators: [FormBuilderValidators.required()],
formField: FormField(
builder: (FormFieldState<dynamic> field) {
return InputDecorator(
decoration: InputDecoration(
prefixIcon: Icon(Icons.location_on),
labelText: "Business type",
errorText: field.errorText,
),
isEmpty: type == '',
child: new DropdownButtonHideUnderline(
child: new DropdownButton(
value: type,
isDense: true,
onChanged: (String newValue) {
setState(() {
type = controller.text = newValue;
field.didChange(newValue);
});
},
items: _type.map(
(String value) {
return new DropdownMenuItem(
value: value,
child: new Text(value),
);
},
).toList(),
),
),
);
},
)),
);
}
Widget _builddocumentnumber1(int keyValue) {
TextEditingController controller = TextEditingController();
_documentControllers.add(controller);
return new TudoTextWidget(
controller: controller,
prefixIcon: Icon(FontAwesomeIcons.idCard),
labelText: "Document Number",
validator: Validators().validateLicenseno,
onSaved: (val) {
setState(() {
documentValues[keyValue] = val;
});
// _licenseno = val;
},
);
}
Widget _builddate(int keyValue) {
TextEditingController controller = TextEditingController();
_expiryDate.add(controller);
return DateTimeField(
format: format,
autocorrect: true,
autovalidate: autovalidate,
controller: controller,
readOnly: true,
decoration: InputDecoration(
labelText: "Expiry Date",
hintText: "Expiry Date",
prefixIcon: Icon(
FontAwesomeIcons.calendar,
size: 24,
)),
onShowPicker: (context, currentValue) {
return showDatePicker(
context: context,
firstDate: DateTime(1900),
initialDate: currentValue ?? DateTime.now(),
lastDate: DateTime.now());
},
);
}
Widget _buildissuingauthority1(int keyValue) {
TextEditingController controller = TextEditingController();
_issuingauthority.add(controller);
return new TudoTextWidget(
prefixIcon: Icon(FontAwesomeIcons.idCard),
labelText: "Issuing Authority",
validator: (val) => Validators.validateName(val, "Issuing Authority"),
onSaved: (val) {
setState(() {
issuingAuthority[keyValue] = val;
});
},
controller: controller,
);
}
Widget _buildidentificationpictures(int keyValue) {
return GridView.count(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 5,
childAspectRatio: 1,
children: List.generate(images.length, (index) {
if (images[index] is ImageUploadModel) {
ImageUploadModel uploadModel = images[index];
return Card(
clipBehavior: Clip.antiAlias,
child: Stack(
children: <Widget>[
Image.file(
uploadModel.imageFile,
width: 300,
height: 300,
),
Positioned(
right: 5,
top: 5,
child: InkWell(
child: Icon(
Icons.remove_circle,
size: 20,
color: Colors.red,
),
onTap: () {
setState(() {
images.replaceRange(index, index + 1, ['Add Image']);
});
},
),
),
],
),
);
} else {
return Card(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
_onAddImageClick(index);
},
),
);
}
}),
);
}
#override
Widget build(BuildContext context) {
final appBar = AppBar(
title: Text("BSP Unlicensed Details"),
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
NavigationHelper.navigatetoBack(context);
},
),
actions: <Widget>[IconButton(icon: Icon(Icons.add), onPressed: _add)],
centerTitle: true,
);
final bottomNavigationBar = Container(
color: Colors.transparent,
height: 56,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new FlatButton.icon(
icon: Icon(Icons.close),
label: Text('Clear'),
color: Colors.redAccent,
textColor: Colors.black,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () {},
),
new FlatButton.icon(
icon: Icon(FontAwesomeIcons.arrowCircleRight),
label: Text('Next'),
color: colorStyles["primary"],
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
textColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () async {
setState(() {
autovalidate = !autovalidate;
});
if (_formKey.currentState.validate()) {
BspSignupCommonModel model = widget.bspSignupCommonModel;
for (var i = 0; i < _myWidgets.length; i++) {
String document = _documentControllers[i].text;
String issuingAuthorityType = _issuingauthoritytype[i].text;
String expiryDate = _expiryDate[i].text;
String issuingAuthority = _issuingauthority[i].text;
// String picture = _identificationpictures[i].text;
print('Document: $document');
print('IssuingAuthorityType: $issuingAuthorityType');
print('ExpiryDate: $expiryDate');
print('IssuingAuthority: $issuingAuthority');
print('Picture: ${_identificationpictures.length}');
print(_myWidgets.length);
List<Licensed> listOfLicenses = new List<Licensed>();
Licensed licensed = new Licensed(
bspLicenseNumber: document,
bspAuthority: issuingAuthority,
bspExpiryDate: expiryDate,
bspIssuing: issuingAuthorityType);
licensed.bspLicenseNumber = _documentControllers[i].text;
licensed.bspExpiryDate = _expiryDate[i].text;
licensed.bspIssuing = _issuingauthoritytype[i].text;
licensed.bspAuthority = _issuingauthority[i].text;
listOfLicenses.add(licensed);
model.unlicensed = listOfLicenses;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BspLicensedSignupTermsPage(
bspSignupCommonModel: model)));
}
}),
],
),
);
return new Scaffold(
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
body: Container(
height: double.infinity,
width: double.infinity,
child: Form(
autovalidate: autovalidate,
key: _formKey,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Expanded(
child: SizedBox(
child: ListView(
padding: const EdgeInsets.all(18.0),
children: _myWidgets,
),
),
),
],
)
],
)),
),
);
}
}
Edit: It seems that you will need to use a Map instead of a List because if you remove an item the indexes could change.
You could do something like this: int keyValue = ++_lastKey;where _lastKey would be a variable that you increment every time you add an item and it will work as unique identifier
Then instead of _myWidgets.removeAt(keyValue); call this _myWidgetsMap.remove(keyValue) inside a setState(), like this:
setState(() {
_myWidgetsMap.remove(keyValue);
});
And you should call the code inside _add() inside a setState()
But I recommend you to build your widgets according to a list of information, instead of a list of widgets. This way when you change the information, the widgets would adapt correctly.
Maybe you can try to draw or not draw the widget if the button is tapped or not, you can create a var in the code to handle the state of the widget and when you tap the close button change the value of the var, something like this:
bool isClosed = false;
!isClosed?MyWidget():Container()
and in the onTap() of the close button you need to include this:
setState(() {
isClosed = true;
});
I hope this help you
class _MyHomePageState extends State<MyHomePage> {
bool isClosed = false;
void Closed() {
setState(() {
isClosed = true;
});
print("CLick");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: !isClosed?Center(
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blueAccent,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
child: Container(
child: Icon(Icons.close),
),
onTap: (){
Closed();
},
),
Text(
'Close Here by Tap',
),
],
),
),
):Container(),
);
}
}
this code works for me
I know I'm late to the party but someone else may find this helpful.
I was faced with exactly the same problem. And I'm proud to say that after a good one hour, I found a solution!
To explain it simply:-
Use a Map<String, Widget> instead of a List (as some have rightly suggested already)
Use UniqueKey().toString() to generate a new key as you dynamically build a widget each time and add this <key, value> pair to the Map
Here's the code snippet
Map<String, MyCard> theCards = {
'0': MyCard(
isInitialCard: true,
)
};
void removeMyCard(String cIndex) {
setState(() {
print(cIndex);
substrateCards.remove(cIndex);
});
}
void addMyCard() {
setState(() {
String id = UniqueKey().toString();
theCards.putIfAbsent(
id,
() => MyCard(
onClose: () {
removeMyCard(id);
},
));
print(id);
});
}
Then, in the ListView.builder, build from the Map, like so:-
ListView.builder(
shrinkWrap: true,
itemCount: theCards.length,
itemBuilder: (BuildContext context, int index) {
String key = theCards.keys.elementAt(index);
return theCards[key];
}),
It will work, whether you delete from the middle, end or wherever.