skip to Main Content

I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.

However, when I try to add a form field in between two form fields the data for the field does not update correctly.

How can I correctly add a field in between the two fields and populate the data correctly?

import 'package:flutter/material.dart';

class DynamicFormWidget extends StatefulWidget {
  const DynamicFormWidget({Key? key}) : super(key: key);

  @override
  State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}

class _DynamicFormWidgetState extends State<DynamicFormWidget> {
  List<String?> names = [null];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Forms'),
      ),
      body: ListView.separated(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
        itemBuilder: (builderContext, index) => Row(
          children: [
            Flexible(
              child: TextFormField(
                initialValue: names[index],

                onChanged: (name) {
                  names[index] = name;
                  debugPrint(names.toString());
                },
                decoration: InputDecoration(
                  hintText: 'Enter your name',
                    border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8))),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8),
              child: IconButton(
                  onPressed: () {
                          setState(() {
                            if(index + 1 == names.length){
                              names.add( null); debugPrint('Added: $names');
                            } else {
                              names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
                            }
                          });


                        },
                  color: Colors.green,
                  iconSize: 32,
                  icon: const Icon(Icons.add_circle)),
            ),
            Padding(
              padding: const EdgeInsets.all(8),
              child: IconButton(
                  onPressed: (index == 0&& names.length == 1)
                      ? null
                      : () {
                    setState(() {
                      names.removeAt(index);
                    });

                    debugPrint('Removed [$index]: $names');
                  },
                  color: Colors.red,
                  iconSize: 32,
                  icon: const Icon(Icons.remove_circle)),
            ),
          ],
        ),
        separatorBuilder: (separatorContext, index) => const SizedBox(
          height: 16,
        ),
        itemCount: names.length,
      ),
    );
  }
}

2

Answers


  1. Basically the problem is that Flutter is confused about who is who in your TextFormField list.

    To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:

    ...
    child: TextFormField(
      initialValue: names[index],
      key: UniqueKey(), // add this line
      onChanged: (name) {
    ...
    

    If you want to learn more about keys and its correct use take a look at this.

    Login or Signup to reply.
  2. The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.

    In this sample i’m making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.

    To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.

    class App extends StatelessWidget {
      final print_all = ChangeNotifier();
    
      App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: FormList(print_notifier: print_all),
            floatingActionButton: IconButton(
              onPressed: print_all.notifyListeners,
              icon: Icon(Icons.checklist),
            ),
          ),
        );
      }
    }
    
    class FormList extends StatefulWidget {
      final ChangeNotifier print_notifier;
      FormList({required this.print_notifier, super.key});
    
      @override
      _FormList createState() => _FormList();
    }
    
    class _FormList extends State<FormList> {
      final _controllers = <TextEditingController>[];
      final _list_key = GlobalKey<AnimatedListState>();
    
      void print_all() {
        for (var controller in _controllers) print(controller.text);
      }
    
      @override
      void initState() {
        super.initState();
        widget.print_notifier.addListener(print_all);
        _controllers.add(TextEditingController(text: 'Inital entrie'));
      }
    
      @override
      void dispose() {
        widget.print_notifier.removeListener(print_all);
        for (var controller in _controllers) controller.dispose();
        super.dispose();
      }
    
      void _insert(int index) {
        final int at = index.clamp(0, _controllers.length - 1);
        _controllers.insert(at, TextEditingController(text: 'Insert at $at'));
        // AnimatedList will take what is placed in [at] so the controller
        // needs to exist before adding the widget
        _list_key.currentState!.insertItem(at);
      }
    
      void _remove(int index) {
        final int at = index.clamp(0, _controllers.length - 1);
        // The widget is replacing the original, it is used to animate the
        // disposal of the widget, ex: size.y -= delta * amount
        _list_key.currentState!.removeItem(at, (_, __) => Container());
        _controllers[at].dispose();
        _controllers.removeAt(at);
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedList(
          key: _list_key,
          initialItemCount: _controllers.length,
          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
          itemBuilder: (ctx, index, _) {
            return CustomLineForm(
              index: index,
              controler: _controllers[index],
              on_insert: _insert,
              on_remove: _remove,
            );
          },
        );
      }
    }
    
    class CustomLineForm extends StatelessWidget {
      final int index;
      final void Function(int) on_insert;
      final void Function(int) on_remove;
      final TextEditingController controler;
    
      const CustomLineForm({
        super.key,
        required this.index,
        required this.controler,
        required this.on_insert,
        required this.on_remove,
      });
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            Flexible(
              child: TextFormField(
                controller: controler,
              ),
            ),
            IconButton(
              icon: Icon(Icons.add_circle),
              onPressed: () => on_insert(index),
            ),
            IconButton(
              icon: Icon(Icons.remove_circle),
              onPressed: () => on_remove(index),
            )
          ],
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search