Search code examples
flutterformsinputspeech-to-textflutter-provider

Flutter: SpeechToTextProvider with Form


I'm going crazy with speech_to_text.

In my app I have various forms, each with various TextFormFields. For each input I have to attach a microphone that allows me to transcribe what is said. I had implemented everything with basic SpeechToText, but it gave some problems due to the various initializations. I then modified everything by switching to SpeechToTextProvider and taking inspiration from this code made available by the plugin.

However, I cannot ensure that the spoken text is transcribed word for word into the input. This, in SpeechToText is handled via the onResult parameter of the listen event, but this is not available on the provider!

Can anyone help me? A thousand thanks!

Here is the code of a form, for simplicity with only one input:

class WeekForm extends StatefulWidget {
  const WeekForm({required this.week, super.key});

  final Week week;

  @override
  State<WeekForm> createState() => _WeekFormState();
}

class _WeekFormState extends State<WeekForm> {
  Week get week => widget.week;

  final _formKey = GlobalKey<FormState>(debugLabel: "WeekForm");

  final TextEditingController _learnedController = TextEditingController();
  String _lastTextChange = '';

  void _setLastTextChange(inputKey, lastText) {
    setState(() {
      _lastTextChange = lastText;
    });
  }

  void _saveLearn(week, appState) async {
    if (_formKey.currentState!.validate()) {
      week.learned = _learnedController.text;

      // save data to Firebase
      appState.setWeek(week);
    }
  }

  @override
  void initState() {
    ///fetched data from firebase showing on controller
    super.initState();
    _learnedController.text = week.learned ?? "";
    _setLastTextChange("learned", _learnedController.text);
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // Add TextFormFields and ElevatedButton here.
          Row(
            children: [
              Expanded(
                child: TextFormField(
                  controller: _learnedController,
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                  ),
                  onChanged: (value) {
                    _setLastTextChange("learned", value);
                  },
                ),
              ),
              SpeechProvider(
                key: const ValueKey("learned"),
                controller: _learnedController,
                lastInputChange: _lastTextChange,
                setLastInputChange: _setLastTextChange,
              ),
            ],
          ),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Align(
              alignment: Alignment.bottomRight,
              child: ElevatedButton(
                onPressed: () => _saveLearn(week, appState),
                child: const Text("Save"),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

And here is the code to call SpeechToTextProvider:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:speech_to_text/speech_to_text_provider.dart';

class SpeechProvider extends StatefulWidget {
  const SpeechProvider({
    super.key,
    required this.controller,
    required this.lastInputChange,
    required this.setLastInputChange,
  });

  final TextEditingController controller;
  final String lastInputChange;
  final void Function(String, String) setLastInputChange;

  @override
  SpeechProviderState createState() => SpeechProviderState();
}

class SpeechProviderState extends State<SpeechProvider> {
  String lastWords = '';
  String lastStatus = '';

  late SpeechToTextProvider speechProvider;

  void startListening() {
    if (speechProvider.isAvailable && speechProvider.isNotListening) {
      lastWords = '';

      speechProvider.listen(
        // I want something like this!!! but this is available only in basic SpeechToText
        onResult: resultListener,
      );
    }
  }

  void stopListening() {
    if (speechProvider.isListening) {
      speechProvider.stop();
      widget.setLastInputChange((widget.key as ValueKey<dynamic>).value, widget.controller.text);
      setState(() {});
    }
  }

  /// This callback is invoked each time new recognition results are
  /// available after `listen` is called.
  void resultListener() {
    var result = speechProvider.lastResult;

    if (result!.finalResult) {
      widget.setLastInputChange((widget.key as ValueKey<dynamic>).value, widget.controller.text);
    } else {
      setState(() {
        lastWords = result.recognizedWords;
      });
      widget.controller.text = '${widget.lastInputChange} $lastWords';
    }
  }

  @override
  Widget build(BuildContext context) {
    speechProvider = Provider.of<SpeechToTextProvider>(context);

    if (speechProvider.isNotAvailable) {
      return const Center(
        child: Text('Speech recognition not available, no permission or not available on the device.'),
      );
    }
    return MicrophoneWidget(speechProvider.isListening, startListening, stopListening, key: UniqueKey());
  }
}

class MicrophoneWidget extends StatelessWidget {
  const MicrophoneWidget(this.isListening, this.startListening, this.stopListening, {super.key});

  final bool isListening;
  final void Function() startListening;
  final void Function() stopListening;

  @override
  Widget build(BuildContext context) {
    return TapRegion(
      onTapOutside: (tap) => isListening ? stopListening() : null,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: SizedBox(
          child: FloatingActionButton(
            onPressed: isListening ? stopListening : startListening,
            shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(80.0)),
            ),
            child: Icon(isListening ? Icons.mic : Icons.mic_off),
          ),
        ),
      ),
    );
  }
}

Solution

  • Based on your description, I think you want to have a way to get a SpeechRecognitionResult whenever there's a new one. SpeechToTextProvider exposes a stream getter which you can listen to it and access the recognition result from the events it emits:

    speechProvider.stream.listen((event) {
      final result = event.recognitionResult;
    });