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:
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!
Finally I found how to integrate speech_to_text
with a Form.
Here is the updated 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 = '';
final SpeechToText speech = SpeechToText();
// I managed `speech_to_text` with the Form through this new variable:
String lastTextChange = '';
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,
setLastTextChange: setLastTextChange,
);
}
});
}),
MicrophoneWidget(speech.isNotListening, startListening, stopListening),
]);
}
void setLastTextChange(lastText) {
lastTextChange = lastText;
}
void startListening() {
lastWords = '';
speech.listen(
onResult: resultListener,
);
setState(() {});
}
void stopListening() {
speech.stop();
setState(() {
lastTextChange = _descriptionController.text;
});
}
void resultListener(SpeechRecognitionResult result) {
if (result.finalResult) {
setState(() {
lastTextChange = _descriptionController.text;
});
} else {
setState(() {
lastWords = result.recognizedWords;
_descriptionController.text = '$lastTextChange $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.descriptionController,
required this.stopListening,
required this.setLastTextChange,
});
final Baby baby;
final String lastWords;
final TextEditingController descriptionController;
final void Function() stopListening;
final void Function(String) setLastTextChange;
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>(debugLabel: 'BabyDescriptionFormState');
void _saveBabyProfile(appState) async {
widget.stopListening();
if (_formKey.currentState!.validate()) {
widget.baby.description = widget.descriptionController.text;
// I save the data in Firebase
appState.setBabyData(widget.baby);
}
}
@override
void initState() {
super.initState();
widget.descriptionController.text = widget.baby.description ?? '';
widget.setLastTextChange(widget.descriptionController.text);
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: widget.descriptionController,
onChanged: (value) {
widget.stopListening();
widget.setLastTextChange(value);
},
),
Consumer<ApplicationState>(
builder: (context, appState, _) => Align(
child: ElevatedButton(
onPressed: () => _saveBabyProfile(appState),
child: const Text("Save"),
),
),
),
],
),
);
}
}