Reputation: 1
I am experimenting with the code for Flutter Form Builder's dynamic forms, which can be found here. (For context, a dynamic form lets you add and remove form fields at will.)
In the original code, when pressing the Delete button associated with a form field, instead of deleting the associated form field as you would expect, it always deletes the FormBuilderTextField that was most recently added.
I wanted to fix this behavior; the solution I came up with did seem to work under the hood. When I print out the formKey's current state after I delete a field and press Submit, it accurately reflects the state I expected it to be in. But this is not reflected in the UI; the text fields are still displayed as if the latest field was deleted.
Form state after pressing Delete on field #3 - both UI and under-the-hood state are inaccurate
Form state after pressing Submit - only UI is inaccurate
My modified code for dynamic_fields.dart from the Flutter Form Builder repo:
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class DynamicFields extends StatefulWidget {
const DynamicFields({Key? key}) : super(key: key);
@override
State<DynamicFields> createState() => _DynamicFieldsState();
}
class _DynamicFieldsState extends State<DynamicFields> {
final _formKey = GlobalKey<FormBuilderState>();
final Map<int, Widget> fields = {};
int idCounter = 1;
String savedValue = '';
@override
void initState() {
savedValue = _formKey.currentState?.value.toString() ?? '';
super.initState();
}
@override
Widget build(BuildContext context) {
return FormBuilder(
key: _formKey,
// IMPORTANT to remove all references from dynamic field when delete
clearValueOnUnregister: true,
child: Column(
children: <Widget>[
const SizedBox(height: 20),
FormBuilderTextField(
name: 'name',
validator: FormBuilderValidators.required(),
decoration: const InputDecoration(
label: Text('Started field'),
),
),
...fields.values,
const SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
child: MaterialButton(
color: Theme.of(context).colorScheme.secondary,
child: const Text(
"Submit",
style: TextStyle(color: Colors.white),
),
onPressed: () {
_formKey.currentState!.saveAndValidate();
setState(() {
savedValue = "Submit button pressed - current form state: ${_formKey.currentState!.value.toString()}";
_formKey.currentState?.value.toString() ?? '';
});
},
),
),
const SizedBox(width: 20),
Expanded(
child: MaterialButton(
color: Theme.of(context).colorScheme.secondary,
child: const Text(
"Add field",
style: TextStyle(color: Colors.white),
),
onPressed: () {
setState(() {
fields[idCounter] = NewTextField(
name: 'name_${idCounter}',
id: idCounter,
onDelete: (int id) {
setState(() {
fields.remove(id);
_formKey.currentState!.save();
print("Removed field #$id - current form state: ${_formKey.currentState!.value.toString()}");
savedValue =
"Removed field #$id - current form state: ${_formKey.currentState!.value.toString()}";
});
},
);
idCounter++;
});
},
),
),
],
),
const Divider(height: 40),
Text(savedValue),
],
),
);
}
}
class NewTextField extends StatelessWidget {
const NewTextField({
super.key,
required this.name,
required this.id,
required this.onDelete,
});
final String name;
final int id;
final void Function(int) onDelete;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Expanded(
child: FormBuilderTextField(
name: name,
decoration: InputDecoration(
label: Text('Field #$id'),
),
),
),
IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () => onDelete(id),
),
],
),
);
}
}
Upvotes: 0
Views: 614
Reputation: 1
Answered my own question after enough digging. Essentially the solution is to give each NewTextField an explicit and unique key when creating them.
From what I understand, without explicit and unique keys, Flutter relies on the order and type of widgets to determine how to update the UI, so removing a widget means Flutter gets confused about which Widget corresponds to which UI element, leading to the UI inaccuracy in my latter two images.
I only had to change one thing in my code; in the onPressed() function for adding new TextFields, I create a new explicit key for each NewTextField:
onPressed: () {
setState(() {
fields[idCounter] = NewTextField(
key: UniqueKey(), // Assign explicit key to each new TextField
name: 'name_${idCounter}',
id: idCounter,
onDelete: (int id) {
setState(() {
fields.remove(id);
_formKey.currentState!.save();
print("Removed field #$id - current form state: ${_formKey.currentState!.value.toString()}");
savedValue =
"Removed field #$id - current form state: ${_formKey.currentState!.value.toString()}";
});
},
);
idCounter++;
});
},
Upvotes: 0