How to manage speech_to_text with a Form

ghz 7months ago ⋅ 67 views

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!

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:

  1. Modify the resultListener method to update the text controller only if the text has not been manually changed by the user.

  2. Add logic to handle changes in the text field manually by setting a flag when the user manually edits the text.

  3. 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.