flutter_form_builder
flutter_form_builder copied to clipboard
Conditional Insertion of FormFields inside a FormBuilder
#Scenario:
I am trying to create a Form
where the form fields are conditional. To do this, I have created a FormBuilderRadioGroup
. This is not inside any FormBuilder
. If the value chosen in this radio group changes, I use a useState
hook to keep track of it.
Depending on this value (called useChosenCategory.value
in the code below), I use the spread
operator to conditionally insert a list of form fields inside a Column
. This column is wrapped in a FormBuilder
and has a key _formGlobalKey
.
To keep this simple, I have taken out excess and useless parts of the code. For context, in any category, there can be three kinds of fields:
- Fields that are the same and have the same name and do not change across categories (like
DateField()
) - Dropdown Fields that may stay the same or their
items
may change depending on the category. To avoid boilerplate, I just manually change these items depending on the category and the field and its name does not stay the same. - Some Fields may appear or disappear depending on the category.
#What I want to do:
If the user enters some value and then changes the category, I want the form to be reset.
#Code:
class AddPage extends HookConsumerWidget {
AddPage({Key? key}) : super(key: key);
final _formGlobalKey = GlobalKey<FormBuilderState>();
final String chosenCategory = '-';
@override
Widget build(BuildContext context, WidgetRef ref) {
final useChosenCategory = useState(chosenCategory);
useEffect(() {
_formGlobalKey.currentState?.reset();
return () {};
}, [useChosenCategory.value]);
final _databaseService = ref.watch(databaseServiceRiverpod);
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
AddcategoryRadio(
// Here I provide the ValueNotifier which is changed in the onChanged method
currentRadioSelection: useChosenCategory,
),
Consumer(builder: (context, ref, child) {
final _menuItems =
ref.watch(itemsFutureRiverpod(dropdownItemTags['all']!));
return _menuItems.when(
data: (data) => FormBuilder(
key: _formGlobalKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (useChosenCategory.value == '-')
const Text('Please choose a category!'),
if (useChosenCategory.value == 'category_1')
...category1Fields(),
if (useChosenCategory.value == 'category_2')
...category2Fields(itemList: data),
if (useChosenCategory.value == 'category_3')
...category3Fields(itemList: data),
if (useChosenCategory.value != '-')
...formFooter(
context: context,
category: useChosenCategory.value,
databaseService: _databaseService,
key: _formGlobalKey,
),
],
),
),
error: (error, stackTrace) {
return const CustomErrorWidget();
},
loading: () {
return const CustomLoadingWidget();
},
);
}),
],
),
);
}
}
List<Widget> category1Fields() => [
const DateField(),
const TextInputField(
name: 'amount',
hintText: 'Enter the Amount',
),
];
List<Widget> category2Fields({required List<ItemModel> itemList}) => [
const DateField(),
DropdownField(
name: 'dropdown_cat_2',
hintText: 'Choose',
listOfItems: itemList
.where(
(element) =>
dropdownItemTags['cat_2']!.contains(element.type),
)
.toList(),
),
const TextInputField(
name: 'amount',
hintText: 'Enter the Amount',
),
];
List<Widget> category3Fields({required List<ItemModel> itemList}) => [
const DateField(),
DropdownField(
name: 'dropdown_cat_3',
hintText: 'Choose',
listOfItems: itemList
.where(
(element) =>
dropdownItemTags['cat_3']!.contains(element.type),
)
.toList(),
),
const TextInputField(
name: 'amount',
hintText: 'Enter the Amount',
),
];
List<Widget> formFooter({
required BuildContext context,
required String category,
required GlobalKey<FormBuilderState> key,
required DatabaseService databaseService,
}) =>
[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CustomButton(
buttontext: 'Reset',
buttonWidth: 300,
onTapFunc: () {
key.currentState!.reset();
},
),
CustomButton(
buttontext: 'Add Transaction',
buttonWidth: 300,
onTapFunc: () async {
// This saves the values from the currentState
key.currentState!.save();
// Check if the values are valid
if (key.currentState!.validate()) {
// if they are valid, send them to the database
final result =
await databaseService.addTransaction(submitTransaction(
category: category,
key: key,
)!);
// This resets the field regardless of the outcome
key.currentState!.reset();
// Checking if the result was successful or not
if (result.isError) {
buildSnackBar(
context: context,
snackBarContent: 'ERROR: Please try again!',
);
} else {
buildSnackBar(
context: context,
snackBarContent: 'SUCCESS: Transaction Added',
);
}
} else {
// When the validation fails
buildSnackBar(
context: context,
snackBarContent: 'Invalid Transaction',
);
}
},
),
],
),
];
TransactionModel? submitTransaction({
required String category,
required GlobalKey<FormBuilderState> key,
}) {
if (category == 'category_1') {
return TransactionModel.category1(
tDate: key.currentState!.value['date'],
amount: key.currentState!.value['amount'],
);
}
if (category == 'category_2') {
return TransactionModel.category2(
tDate: key.currentState!.value['date'],
credAcc: key.currentState!.value['dropdown_cat_2'],
amount: key.currentState!.value['amount'],
);
}
if (category == 'category_3') {
return TransactionModel.category3(
tDate: key.currentState!.value['date'],
credAcc: key.currentState!.value['dropdown_cat_3'],
amount: key.currentState!.value['amount'],
);
}
return null;
}
#Error
This error is encountered when I try to "navigate" between different categories. Though, It only happens when I try to switch multiple times.
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
'package:flutter_form_builder/src/form_builder.dart': Failed assertion: line 187 pos 12: '_fields.containsKey(name)': is not true.
package:flutter_form_builder/src/form_builder.dart:187
When the exception was thrown, this was the stack
#2 FormBuilderState.unregisterField
package:flutter_form_builder/src/form_builder.dart:187
#3 FormBuilderFieldState.dispose
package:flutter_form_builder/src/form_builder_field.dart:157
#4 _FormBuilderTextFieldState.dispose
package:flutter_form_builder/…/fields/form_builder_text_field.dart:450
#5 StatefulElement.unmount
package:flutter/…/widgets/framework.dart:4983
#6 _InactiveElements._unmount
package:flutter/…/widgets/framework.dart:1926
#7 _InactiveElements._unmount.<anonymous closure>
package:flutter/…/widgets/framework.dart:1924
#8 SingleChildRenderObjectElement.visitChildren
package:flutter/…/widgets/framework.dart:6271
#9 _InactiveElements._unmount
package:flutter/…/widgets/framework.dart:1922
#10 _InactiveElements._unmount.<anonymous closure>
package:flutter/…/widgets/framework.dart:1924
#11 ComponentElement.visitChildren
package:flutter/…/widgets/framework.dart:4807
#12 _InactiveElements._unmount
package:flutter/…/widgets/framework.dart:1922
#13 ListIterable.forEach (dart:_internal/iterable.dart:39:13)
#14 _InactiveElements._unmountAll
package:flutter/…/widgets/framework.dart:1935
#15 BuildOwner.lockState
package:flutter/…/widgets/framework.dart:2519
#16 BuildOwner.finalizeTree
package:flutter/…/widgets/framework.dart:2932
#17 WidgetsBinding.drawFrame
package:flutter/…/widgets/binding.dart:884
#18 RendererBinding._handlePersistentFrameCallback
package:flutter/…/rendering/binding.dart:363
#19 SchedulerBinding._invokeFrameCallback
package:flutter/…/scheduler/binding.dart:1144
#20 SchedulerBinding.handleDrawFrame
package:flutter/…/scheduler/binding.dart:1081
#21 SchedulerBinding._handleDrawFrame
package:flutter/…/scheduler/binding.dart:995
#25 _invoke (dart:ui/hooks.dart:151:10)
#26 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#27 _drawFrame (dart:ui/hooks.dart:115:31)
(elided 5 frames from class _AssertionError and dart:async)
════════════════════════════════════════════════════════════════════════════════
I realize that the most probable issue is that I am trying to insert fields with the same name inside the widget tree. I do not know how to fix this without tons and tons of boilerplate.
Let me know if any other part of my code is required.
Any help would be appreciated.
My case: Occurs when a widget is deleted from the text field widget array.
Error: assert(_fields.containsKey(name)); ======== Exception caught by widgets library ======================================================= The following assertion was thrown while finalizing the widget tree: Assertion failed: file:///Users/patrick386/.pub-cache/hosted/pub.dartlang.org/flutter_form_builder-7.6.0/lib/src/form_builder.dart:194:12 _fields.containsKey(name) is not true
void unregisterField(String name, FormBuilderFieldState field) {
assert(_fields.containsKey(name));
// Only remove the field when it is the one registered. It's possible that
// the field is replaced (registerField is called twice for a given name)
// before unregisterField is called for the name, so just emit a warning
// since it may be intentional.
if (field == _fields[name]) {
_fields.remove(name);
_transformers.remove(name);
if (widget.clearValueOnUnregister) {
_instantValue.remove(name);
_savedValue.remove(name);
}
} else {
assert(() {
// This is OK to ignore when you are intentionally replacing a field
// with another field using the same name.
debugPrint('Warning! Ignoring Field unregistration for $name'
' -- this is OK to ignore as long as the field was intentionally replaced');
return true;
}());
}
}
My code: RiverPod StateNotifier
OptionPriceBlockData block = widget.priceOprionBlcok;
....
FormBuilderTextField(
name: block.id, // << Uuid.V4
controller: block.controller,
...
);
deleteItem(OptionPriceBlockData b) {
b.titleController?.dispose();
b.priceController?.dispose();
state = state.copyWith(optionPriceBlocks: [
for (OptionPriceBlockData block in state.optionPriceBlocks)
if (!block.id.contains(b.id)) block
]);
}