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,
)
],
),
);
}
}
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,
),
],
),
);
}
}