skip to Main Content

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:

  1. microphone-on, speak, edit, speak
  2. 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!

2

Answers


  1. Chosen as BEST ANSWER

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

  2. Here is the re-produced code. I Tried these sequences 1) microphone-on, speak, edit, speak 2) mic-on, speak, mic-off, edit, mic-on, speak. And it’s working fine. Replace the Baby and ApplicationState with your original code.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search