1

I have a chat-app that uses a single TextField controlled by a TextEditingController to enter text messages. Pressing the associated IconButton sends the message if the message is not empty and then clears the TextEditingController. This all works perfectly. After sending the message, the text input field gets cleared.

BUT, here comes the bug, if I press the send button again, the message is sent once more. How come and how can I prevent this?

class NewMessage extends StatefulWidget {
  @override
  _NewMessageState createState() => _NewMessageState();
}

class _NewMessageState extends State<NewMessage> {
  final _controller = TextEditingController();
  var _enteredMessage = '';

  void _sendMessage() async {
    FocusScope.of(context).unfocus();
    final user = FirebaseAuth.instance.currentUser;
    final userData = await FirebaseFirestore.instance
        .collection('users')
        .doc(user.uid)
        .get();
    FirebaseFirestore.instance.collection('chat').add({
      'text': _enteredMessage,
      'createdAt': Timestamp.now(),
      'userId': user.uid,
      'username': userData.data()['username'],
      'userImage': userData.data()['image_url']
    });
    _controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 8),
      padding: EdgeInsets.all(8),
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              controller: _controller,
              textCapitalization: TextCapitalization.sentences,
              autocorrect: true,
              enableSuggestions: true,
              decoration: InputDecoration(labelText: 'Send a message...'),
              onChanged: (value) {
                setState(() {
                  _enteredMessage = value;
                });
              },
            ),
          ),
          IconButton(
            color: Theme.of(context).primaryColor,
            icon: Icon(
              Icons.send,
            ),
            onPressed: _enteredMessage.trim().isEmpty ? null : _sendMessage,
          )
        ],
      ),
    );
  }
}

    
CEO tech4lifeapps
  • 885
  • 1
  • 12
  • 31

3 Answers3

2

The use of a TextEditingController AND an onChanged event for a TextField can be problematic. The issue is discussed in depth here: TextEditingController vs OnChanged

In my case, I finally decided to go for an TextEditingController only solution. This way, we can get rid of the _enteredMessage variable and the onChanged/setState() statements alltogether.

Instead, we need to add a listener to our TextEditingController and call setState() in our initState() method.

Finally, we need to dispose the _controller in the dispose() method to prevent memory leaks.

Here is the code of my TextEditingController only solution:

class NewMessage extends StatefulWidget {
  @override
  _NewMessageState createState() => _NewMessageState();
}

class _NewMessageState extends State<NewMessage> {
  var _controller = TextEditingController();

  @override
  void initState() {
    _controller = TextEditingController();
    _controller.addListener(() {
      setState(() {});
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _sendMessage() async {
    FocusScope.of(context).unfocus();
    final user = FirebaseAuth.instance.currentUser;
    final userData = await FirebaseFirestore.instance
        .collection('users')
        .doc(user.uid)
        .get();
    FirebaseFirestore.instance.collection('chat').add({
      'text': _controller.text,
      'createdAt': Timestamp.now(),
      'userId': user.uid,
      'username': userData.data()['username'],
      'userImage': userData.data()['image_url']
    });
    _controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 8),
      padding: EdgeInsets.all(8),
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              controller: _controller,
              textCapitalization: TextCapitalization.sentences,
              autocorrect: true,
              enableSuggestions: true,
              decoration: InputDecoration(labelText: 'Send a message...'),
            ),
          ),
          IconButton(
            color: Theme.of(context).primaryColor,
            icon: Icon(
              Icons.send,
            ),
            onPressed: _controller.text.trim().isEmpty ? null : _sendMessage,
          ),
        ],
      ),
    );
  }
}

    
CEO tech4lifeapps
  • 885
  • 1
  • 12
  • 31
1

You are clearing the controller in the button callbkack _controller.clear(), but what you are really sending to Firebase is not the _controller text but rather the variable _enteredMessage which does not get cleared.

if you just send the controller text instead of _enteredMessage the problem should be solved:

    FirebaseFirestore.instance.collection('chat').add({
      'text': _controller.text,
      'createdAt': Timestamp.now(),
      'userId': user.uid,
      'username': userData.data()['username'],
      'userImage': userData.data()['image_url']
    });

Also always dispose your controllers in the Stateful Widget onDispose method to avoid memory leaks.

EDIT: The condition on which the button callback gets called should also change to:

...
onPressed: _controller.text.isEmpty ? null : _sendMessage
...
croxx5f
  • 5,163
  • 2
  • 15
  • 36
  • This simply results in the `onPressed` method sending an empty message to Firebase when the send button is pressed for the second time and thereafter. – CEO tech4lifeapps Mar 09 '21 at 05:55
  • 1
    You should change the condition of your onPressed button accordingly: `onPressed: _controller.text.isEmpty ? null : _sendMessage,` – croxx5f Mar 09 '21 at 09:35
  • - croxx5f Of course I have tried that as well. The result is the same. In fact it's exactly the statement you mention that - for some strange reason - does not work. – CEO tech4lifeapps Mar 09 '21 at 14:25
1

I've found the easiest solution here is to replace

 onPressed: _enteredMessage.trim().isEmpty ? null : _sendMessage,

with

 onPressed: () {
            if (_controller.text.trim().isNotEmpty) _sendMessage();
          }
         
CEO tech4lifeapps
  • 885
  • 1
  • 12
  • 31