0

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> 
Roxx
  • 3,738
  • 20
  • 92
  • 155

2 Answers2

1

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: 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.


void
  • 12,787
  • 3
  • 28
  • 42
0

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

EdwynZN
  • 4,895
  • 2
  • 12
  • 15