Search code examples
flutterstateflutter-textformfield

Why does text form field re-render when user clicks on it in Flutter?


Why does text form field re-render when user clicks on it in Flutter?

My Flutter form contains a TextFormField for Name. When my user clicks on it, the entire form immediately re-renders (reloads), making it impossible for my user to enter anything.

Code

The TextFormField is commented so that you can easily find it.

You'll notice that this page also contains a second field that works perfectly. It's a Switch inside a StatefulBuilder that handles setting a TextEditingController for _importantController.

<!-- language: flutter -->
    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:dropdown_search/dropdown_search.dart';
    import 'package:flutter/material.dart';
    
    class CrudPage2 extends StatefulWidget {
      final String docId;
      const CrudPage2({Key? key, required this.docId}) : super(key: key);
    
      @override
      CrudPage2State createState() => CrudPage2State();
    }
    
    class CrudPage2State extends State<CrudPage2> {
    
      late String name = "";
      late bool isImportant = false;
    
      final TextEditingController _nameController = TextEditingController();
      final TextEditingController _importantController = TextEditingController();

      Stream<DocumentSnapshot<Object?>> groceryItem(docID) =>
        FirebaseFirestore.instance
          .collection("groceries")
          .doc(docID)
          .snapshots();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () => Navigator.of(context).pop(),
            ),
            title: Text("Grocery Item"),
          ),
          body: SizedBox(
            width: double.infinity,
            child: Padding(
                padding: EdgeInsets.only(
                    bottom: MediaQuery.of(context).viewInsets.bottom + 20),

                child: StreamBuilder<DocumentSnapshot>(
                    stream: groceryItem(widget.docId),
                    builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> streamSnapshot) {
    
                      if (streamSnapshot.connectionState == ConnectionState.waiting) {
                        print("****** Loading ******");  // debugging
                        return const Text("Loading");
    
                      } else if (streamSnapshot.hasData) {
    
                        if (widget.docId != "NEW") {
                          // Retrieve existing item
                          var jsonData = streamSnapshot.data?.data();
                          Map<String, dynamic> myData = jsonData as Map<String, dynamic>;
                          name = myData['name'];
                          isImportant = myData['important'];
                        }
    
                        _nameController.text = name;
                        if (isImportant) {
                          _importantController.text = "true";
                        } else {
                          _importantController.text = "false";
                        }
    
                        return Column(
                          mainAxisSize: MainAxisSize.min,
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
    
                            //--------------------------------------------------------
                            // PROBLEM: Clicking on this field re-renders entire form.
                            Flexible(
                              child: TextFormField(
                                controller: _nameController,
                                decoration: const InputDecoration(labelText: 'Name'),
                              ),
                            ),
                            //--------------------------------------------------------
    
                            // No problem with this switch
                            StatefulBuilder(
                              builder: (BuildContext context, StateSetter importantStateSetter) {
                                return Row(
                                  children: [
                                    const Text("Important: "),
                                    Switch(
                                      value: isImportant,
                                      onChanged: (value) {
                                        importantStateSetter(() => isImportant = value);
                                      },
                                    ),
                                  ],
                                );
                              },
                            ),
    
                            Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                SizedBox(
                                  child: ElevatedButton(
                                      child: const Text('Cancel'),
                                      onPressed: () async {
                                        Navigator.of(context).pop();
                                      }),
                                ),
                                const SizedBox(
                                  width: 10,
                                ),
                                SizedBox(
                                  child: ElevatedButton(
                                    child: const Text("Submit"),
                                    onPressed: () async {
                                      final String name = _nameController.text;
    
                                      if (widget.docId == 'NEW') {
                                        addGroceryItem(name, 1.0, "test",
                                            isImportant);
                                      } else {
                                        updateGroceryItem(widget.docId, name,
                                            1.0, "test", isImportant);
                                      }
    
                                      // Clear the text fields
                                      _nameController.text = '';
                                      _importantController.text = "";
    
                                      // Hide the bottom sheet
                                      Navigator.of(context).pop();
    
                                    },
                                  ),
                                )
                              ],
                            ),
                          ],
                        );
                      } else {
                        return const Text("No Data");
                      }
                    })
    
            ),
          ),
        );
      } // Widget Build
    
      //-------------------------------------------------------------
      // Add New Grocery Item
      //-------------------------------------------------------------
      Future<void> addGroceryItem(
          String name, double quantity, String category, bool isImportant) async {
        await FirebaseFirestore.instance.collection('groceries').add({
          "active": true,
          "name": name,
          "quantity": quantity,
          "category": category,
          "important": isImportant
        });
      }
    
      //-------------------------------------------------------------
      // Update Existing Grocery Item
      //-------------------------------------------------------------
      Future<void> updateGroceryItem(String docID, String name, double quantity,
          String category, bool isImportant) async {
        await FirebaseFirestore.instance.collection('groceries').doc(docID).update({
          "active": true,
          "name": name,
          "quantity": quantity,
          "category": category,
          "important": isImportant
        });
      }
    
    }

I added print("****** Loading ******"); line to help debug. When user clicks on text form field, the Console displays:

I/flutter (28767): ****** Loading ******
I/flutter (28767): ****** Loading ******

Why is the stream refreshing every time this widget is clicked?

Thank you for your time!


Solution

  • After a lot of Googling, I decided that my problem was coming from doing this entirely wrong. Here are some of the changes I made:

    1. Pass-in values as JSON object parameter
    2. Eliminate call to Firebase
    3. Eliminate Stream Builder

    Code below solves my problem using these changes:

    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:flutter/material.dart';
    
    class CrudPage2 extends StatefulWidget {
      final String docId;
      final Object? docSnap;
      const CrudPage2({Key? key,
        required this.docId,
        required this.docSnap})
          : super(key: key);
    
      @override
      CrudPage2State createState() => CrudPage2State();
    }
    
    class CrudPage2State extends State<CrudPage2> {
    
      //--- Form State Variables...
      late String name = "";
      late bool isImportant = false;
    
      //--- Controllers for Form Fields...
      final TextEditingController _nameController = TextEditingController();
      final TextEditingController _importantController = TextEditingController();
      
      @override
      initState() {
        super.initState();
    
        if (widget.docId != "NEW") {
          Map<String, dynamic> myData = widget.docSnap as Map<String, dynamic>;
          name = myData['name'];
          isImportant = myData['important'];
        }
    
        _nameController.text = name;
    
        if (isImportant) {
          _importantController.text = "true";
        } else {
          _importantController.text = "false";
        }
    
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () => Navigator.of(context).pop(),
            ),
            title: Text("Grocery Item"),
          ),
          body: SizedBox(
            width: double.infinity,
            child: Padding(
                padding: EdgeInsets.only(
                    top: 20,
                    left: 20,
                    right: 20,
                    bottom: MediaQuery.of(context).viewInsets.bottom + 20),
                child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
    
                      TextFormField(
                        controller: _nameController,
                        decoration: const InputDecoration(labelText: 'Name'),
                      ),
    
                      StatefulBuilder(
                        builder:
                            (BuildContext context, StateSetter importantStateSetter) {
                          return Row(
                            children: [
                              const Text("Important: "),
                              Switch(
                                value: isImportant,
                                onChanged: (value) {
                                  importantStateSetter(() => isImportant = value);
                                },
                              ),
                            ],
                          );
                        },
                      ),
    
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
    
                          SizedBox(
                            width: 110,
                            child: ElevatedButton(
                                style: ButtonStyle(
                                    backgroundColor:
                                    MaterialStateProperty.all(
                                        Colors.grey),
                                    padding: MaterialStateProperty.all(
                                        const EdgeInsets.all(5)),
                                    textStyle: MaterialStateProperty.all(
                                        const TextStyle(fontSize: 24))),
                                child: const Text('Cancel'),
                                onPressed: () async {
                                  Navigator.of(context).pop();
                                }),
                          ),
    
                          SizedBox(
                            width: 200,
                            child: ElevatedButton(
                              style: ButtonStyle(
                                  backgroundColor:
                                  MaterialStateProperty.all(Colors.green),
                                  padding: MaterialStateProperty.all(
                                      const EdgeInsets.all(5)),
                                  textStyle: MaterialStateProperty.all(
                                      const TextStyle(fontSize: 24))),
                              child: const Text("Submit"),
                              onPressed: () async {
                                final String name = _nameController.text;
    
                                if (widget.docId == 'NEW') {
                                  addGroceryItem(name, 1.0, "Test",
                                      isImportant);
                                } else {
                                  updateGroceryItem(widget.docId, name,
                                      1.0, "Test", isImportant);
                                }
    
                                // Clear the text fields
                                _nameController.text = '';
                                _importantController.text = "";
    
                                // Hide the bottom sheet
                                Navigator.of(context).pop();
    
                              },
                            ),
                          )
                        ],
                      ),
    
                  ]
                )
              )
            ),
          
        );
    
      } // Widget Build
    
      Future<void> addGroceryItem(
          String name, double quantity, String category, bool isImportant) async {
        await FirebaseFirestore.instance.collection('groceries').add({
          "active": true,
          "name": name,
          "quantity": quantity,
          "category": category,
          "important": isImportant
        });
      }
    
      Future<void> updateGroceryItem(String docID, String name, double quantity,
          String category, bool isImportant) async {
        await FirebaseFirestore.instance.collection('groceries').doc(docID).update({
          "active": true,
          "name": name,
          "quantity": quantity,
          "category": category,
          "important": isImportant
        });
      }
    
    }
    
    

    Any comments and/or suggestions are still appreciated.

    I hope this helps someone else in the future.