← Back to posts Cover image for Voice-Enabling Your Flutter App: A Practical Guide to Speech-to-Text Integration

Voice-Enabling Your Flutter App: A Practical Guide to Speech-to-Text Integration

· 6 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Voice input is no longer a futuristic gimmick; it’s a powerful way to enhance user interaction, especially in mobile apps. Imagine users dictating notes, searching for items, or filling forms without typing a single character. Integrating speech-to-text (STT) can make your Flutter app more accessible, faster to use, and simply more delightful.

In this guide, we’ll walk through seamlessly adding STT functionality to your Flutter application using the popular speech_to_text package. We’ll cover everything from initial setup to populating a TextFormField and handling common challenges.

Getting Started: Setting Up speech_to_text

First things first, let’s add the package to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  speech_to_text: ^6.6.0 # Use the latest version

Run flutter pub get to fetch the package.

Next, you’ll need to configure platform-specific permissions:

Android: Open android/app/src/main/AndroidManifest.xml and add these permissions inside the <manifest> tag:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<queries>
  <intent>
    <action android:name="android.speech.action.RECOGNIZE_SPEECH" />
  </intent>
</queries>

The <queries> tag is important for Android 11+ to ensure your app can discover the speech recognition service.

iOS: Open ios/Runner/Info.plist and add the following keys. These strings are displayed to the user when your app requests permission.

<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to your microphone to enable speech recognition.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition to convert your voice to text.</string>

Basic Speech-to-Text Implementation

With the setup complete, let’s dive into the code. We’ll use a StatefulWidget to manage the speech recognition state.

import 'package:flutter/material.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:speech_to_text/speech_recognition_result.dart';

class VoiceInputScreen extends StatefulWidget {
  const VoiceInputScreen({super.key});

  @override
  State<VoiceInputScreen> createState() => _VoiceInputScreenState();
}

class _VoiceInputScreenState extends State<VoiceInputScreen> {
  final SpeechToText _speechToText = SpeechToText();
  bool _speechEnabled = false;
  String _lastWords = '';
  final TextEditingController _textController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _initSpeech();
  }

  /// Initialize speech recognition.
  /// Checks if the device supports speech recognition and requests permissions.
  void _initSpeech() async {
    _speechEnabled = await _speechToText.initialize(
      onStatus: (status) => print('Speech status: $status'),
      onError: (error) => print('Speech error: $error'),
    );
    setState(() {});
  }

  /// Start listening for speech.
  /// The `onResult` callback is fired continuously with partial and final results.
  void _startListening() async {
    _lastWords = ''; // Clear previous words for a new session
    await _speechToText.listen(
      onResult: _onSpeechResult,
      listenFor: const Duration(seconds: 30), // Max listening duration
      pauseFor: const Duration(seconds: 3),   // Pause before stopping automatically
      localeId: 'en_US', // Specify locale if needed
    );
    setState(() {});
  }

  /// Stop listening for speech.
  void _stopListening() async {
    await _speechToText.stop();
    setState(() {});
  }

  /// This callback is invoked when speech recognition results are available.
  void _onSpeechResult(SpeechRecognitionResult result) {
    setState(() {
      _lastWords = result.recognizedWords;
      // Update the TextEditingController with the recognized words
      _textController.text = _lastWords;
    });
  }

  @override
  void dispose() {
    _textController.dispose();
    _speechToText.cancel(); // Cancel any ongoing listening
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Voice Input Demo'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: TextFormField(
                controller: _textController,
                decoration: InputDecoration(
                  labelText: 'Speak here',
                  hintText: 'Press the mic button and start speaking',
                  border: OutlineInputBorder(),
                ),
                maxLines: 5,
              ),
            ),
            Expanded(
              child: Container(
                padding: const EdgeInsets.all(16),
                alignment: Alignment.bottomCenter,
                child: Text(
                  'Recognized: $_lastWords',
                  style: const TextStyle(fontSize: 20.0),
                ),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _speechToText.isListening ? _stopListening : _startListening,
        tooltip: 'Listen',
        child: Icon(
          _speechToText.isNotListening ? Icons.mic_off : Icons.mic,
          color: _speechToText.isListening ? Colors.red : Colors.white,
        ),
      ),
    );
  }
}

Populating a TextFormField with Voice

In the example above, we directly connected the _textController to the _onSpeechResult callback. Each time the speech recognition engine provides a new result (even partial ones), _textController.text is updated. This gives a real-time, dynamic feel to the input field, allowing users to see their words appear as they speak.

Key points for TextFormField integration:

  • TextEditingController: This is your bridge between the speech recognition results and the TextFormField. Create an instance and pass it to the controller property of your TextFormField.
  • Real-time Updates: The _onSpeechResult callback provides SpeechRecognitionResult objects. The recognizedWords property contains the current best guess of what the user is saying. Updating the _textController.text here gives that “live typing” effect.
  • Cursor Position: When you set _textController.text, the cursor typically jumps to the end. This is usually the desired behavior for voice input.

Enhancing User Experience & Common Challenges

  1. Visual Feedback: It’s crucial to let the user know when the app is listening. In our example, the microphone icon changes from mic_off to mic and its color changes to red when _speechToText.isListening is true. You could also add a pulsating animation or a simple “Listening…” text.

  2. Continuous Input (Appending Text): The provided code overwrites _lastWords and thus _textController.text with each new speech result. If you want to allow users to pause, then speak again and append to the existing text, you’d modify _startListening and _onSpeechResult. One approach:

    • When starting a new listening session, capture the _textController.text content.
    • In _onSpeechResult, append the new result.recognizedWords to the captured text.

    For example, you could modify _startListening to:

    String _currentPrefix = ''; // Store text already in the field
    void _startListening() async {
      _currentPrefix = _textController.text; // Capture current content
      _lastWords = ''; // Clear for new session's recognized words
      await _speechToText.listen(onResult: _onSpeechResult, /* ... */);
      setState(() {});
    }
    
    void _onSpeechResult(SpeechRecognitionResult result) {
      setState(() {
        _lastWords = result.recognizedWords;
        _textController.text = _currentPrefix + ' ' + _lastWords;
        _textController.selection = TextSelection.fromPosition(TextPosition(offset: _textController.text.length)); // Keep cursor at end
      });
    }

    This allows a user to say “Hello”, stop, then press mic again and say “World” to get “Hello World”.

  3. Error Handling: Speech recognition can fail due to network issues, no microphone access, or no speech engine available. The _speechToText.initialize method takes onError and onStatus callbacks. Make sure to log or display user-friendly messages for these scenarios. For instance, if _speechEnabled is false, you could disable the microphone button and show a message.

  4. Locale Selection: The listen method allows you to specify a localeId (e.g., 'en_US', 'es_ES'). This is crucial for accurate recognition in different languages. You can get available locales using _speechToText.locales().

  5. Listening Duration: The listenFor and pauseFor parameters in _speechToText.listen() are very useful. listenFor defines the maximum time the listener will be active, and pauseFor specifies how long of a pause indicates the end of a sentence, leading to an automatic stop. Tuning these can greatly improve the user experience.

Conclusion

Integrating speech-to-text into your Flutter app opens up a world of possibilities for user interaction. The speech_to_text package makes this powerful feature surprisingly straightforward to implement. By providing clear visual feedback, handling continuous input gracefully, and addressing potential errors, you can create a truly intuitive and accessible voice-enabled experience for your users. Happy coding!

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Developers are curious about Flutter's capabilities beyond typical business apps, especially for demanding desktop applications like CAD/CAM or image/video processing. This post will explore Flutter's suitability for high-performance, viewport-based desktop GUIs, discussing Dart's memory model, the 60fps update loop, and real-world examples to gauge its readiness for 'serious' complex software.

Cover image for Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Flutter web applications often suffer from a frustrating 'deep link refresh bug' where refreshing the browser on a nested route (e.g., /home/details) bounces the user back to the root or an incorrect path. This post will diagnose the common causes of this issue, explain how Flutter's router handles web URLs, and provide practical solutions and best practices for building robust, refresh-proof navigation in your Flutter web apps.

Cover image for Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

As Flutter applications grow, managing strings for multiple languages or just keeping text consistent becomes a challenge. This post will guide developers through effective strategies for centralizing strings, implementing robust internationalization (i18n) and localization (l10n), and leveraging tools to streamline the process for small to large-scale projects.