mtao
mtao

Reputation: 1

Flutter Form Builder dynamic form fields not updating properly

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.

Initial form state

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

Answers (1)

mtao
mtao

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

Related Questions