In my Flutter application I have a form that saves data to Firestore. The user must be able to enter data by writing or speaking. To do this, I have attached the speech_to_text plugin to the form.
The problem is that I haven't found a way to manage speaking and writing together: for example, if the user speaks, then modifies the text, then continues speaking, how can I keep the text properly updated in the TextFormField
?
For example, I cannot manage these sequences:
- microphone-on, speak, edit, speak
- mic-on, speak, mic-off, edit, mic-on, speak
Here is my code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
class Speech extends StatefulWidget {
const Speech({super.key});
@override
State<Speech> createState() => _SpeechState();
}
class _SpeechState extends State<Speech> {
bool _hasSpeech = false;
String lastWords = '';
String lastStatus = '';
final SpeechToText speech = SpeechToText();
bool textChanged = false;
final TextEditingController _descriptionController = TextEditingController();
@override
void initState() {
super.initState();
_initSpeechState();
}
Future<void> _initSpeechState() async {
try {
bool hasSpeech = await speech.initialize();
if (!mounted) return;
setState(() {
_hasSpeech = hasSpeech;
});
} catch (e) {
setState(() {
_hasSpeech = false;
});
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
// ApplicationState is the widget with the state of my app
Consumer<ApplicationState>(builder: (context, appState, _) {
return FutureBuilder<Baby>(
future: appState.getBabyData(), // I recover the data from Firestore
builder: (BuildContext context, AsyncSnapshot<Baby> snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
} else {
return MyForm(
baby: snapshot.requireData,
lastWords: lastWords,
descriptionController: _descriptionController,
stopListening: stopListening,
textChanged: textChanged,
setTextChanged: setTextChanged,
);
}
});
}),
MicrophoneWidget(speech.isNotListening, startListening, stopListening),
]);
}
void setTextChanged(changed) {
setState(() {
textChanged = changed;
});
}
void startListening() {
lastWords = '';
speech.listen(
onResult: resultListener,
);
setState(() {
textChanged = false;
});
}
void stopListening() {
speech.stop();
setState(() {
textChanged = false;
});
}
void resultListener(SpeechRecognitionResult result) {
setState(() {
lastWords = result.recognizedWords;
_descriptionController.text = lastWords;
});
}
}
class MicrophoneWidget extends StatelessWidget {
const MicrophoneWidget(this.isNotListening, this.startListening, this.stopListening, {super.key});
final bool isNotListening;
final void Function() startListening;
final void Function() stopListening;
@override
Widget build(BuildContext context) {
return SizedBox(
child: FloatingActionButton(
onPressed: isNotListening ? startListening : stopListening,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(80.0)),
),
child: Icon(isNotListening ? Icons.mic_off : Icons.mic),
),
);
}
}
class MyForm extends StatefulWidget {
const MyForm({
super.key,
required this.baby,
required this.lastWords,
required this.textChanged,
required this.setTextChanged,
required this.descriptionController,
required this.stopListening,
});
final Baby baby;
final String lastWords;
final bool textChanged;
final TextEditingController descriptionController;
final void Function() stopListening;
final void Function(bool) setTextChanged;
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>(debugLabel: 'MyFormState');
void _saveBabyProfile(appState) async {
widget.stopListening();
if (_formKey.currentState!.validate()) {
widget.baby.description = widget.descriptionController.text;
// I save the data in Firestore
appState.setBabyData(widget.baby);
}
}
@override
Widget build(BuildContext context) {
if (!widget.textChanged) {
widget.descriptionController.text = widget.lastWords;
if (widget.baby.description != null) {
widget.descriptionController.text = '${widget.baby.description} ${widget.lastWords}';
}
}
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: widget.descriptionController,
onChanged: (value) {
widget.setTextChanged(true);
},
),
Consumer<ApplicationState>(
builder: (context, appState, _) => ElevatedButton(
onPressed: () => _saveBabyProfile(appState),
child: const Text("Save"),
),
),
],
),
);
}
}
Thanks for your help in advance!
Answers
To manage speaking and writing together in your Flutter application, you can update the TextEditingController
when the user speaks, edits the text manually, or switches between speaking and editing modes. Here's how you can achieve this:
-
Modify the
resultListener
method to update the text controller only if the text has not been manually changed by the user. -
Add logic to handle changes in the text field manually by setting a flag when the user manually edits the text.
-
Clear the flag when the user starts speaking to ensure that the text is updated from the speech recognition results.
Here's the updated code with these changes:
class _SpeechState extends State<Speech> {
// Other existing code...
bool _manuallyChanged = false;
// Other existing code...
void resultListener(SpeechRecognitionResult result) {
if (!_manuallyChanged) {
setState(() {
lastWords = result.recognizedWords;
_descriptionController.text = lastWords;
});
}
}
// Other existing code...
@override
Widget build(BuildContext context) {
return Column(
children: [
// Other existing code...
MyForm(
// Other existing parameters...
textChanged: _manuallyChanged,
setTextChanged: (changed) {
setState(() {
_manuallyChanged = changed;
});
},
),
// Other existing code...
],
);
}
}
class MyForm extends StatefulWidget {
const MyForm({
// Other existing parameters...
required this.textChanged,
required this.setTextChanged,
});
// Other existing parameters...
final bool textChanged;
final void Function(bool) setTextChanged;
// Other existing code...
}
class _MyFormState extends State<MyForm> {
// Other existing code...
@override
Widget build(BuildContext context) {
if (!widget.textChanged) {
widget.descriptionController.text = widget.lastWords;
if (widget.baby.description != null) {
widget.descriptionController.text =
'${widget.baby.description} ${widget.lastWords}';
}
}
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: widget.descriptionController,
onChanged: (value) {
widget.setTextChanged(true);
},
),
// Other existing code...
],
),
);
}
}
With these changes, the text field will be updated based on the speech recognition results only if the user has not manually edited the text. When the user manually edits the text, the flag _manuallyChanged
is set to true, indicating that the text has been changed manually. The flag is cleared when the user starts speaking, allowing the text to be updated from the speech recognition results.