SqueezeOJ
SqueezeOJ

Reputation: 527

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!

Upvotes: 0

Views: 598

Answers (1)

SqueezeOJ
SqueezeOJ

Reputation: 527

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.

Upvotes: 1

Related Questions