You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何在Form中协同管理speech_to_text与手动输入?

解决方案

你的问题核心在于语音输入时未保留手动编辑的内容,以及构建方法中错误的文本重置逻辑导致交互冲突。以下是针对性的修改方案:

核心问题分析

  1. 原代码在启动语音时直接清空lastWords,并在识别结果中覆盖输入框文本,导致手动编辑的内容丢失。
  2. MyForm的build方法每次触发时都会重置输入框文本,引发不必要的内容覆盖。
  3. textChanged变量的状态管理逻辑混乱,导致语音与手动输入的状态冲突。

修改后的完整代码

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();
  final TextEditingController _descriptionController = TextEditingController();
  // 记录语音开始前的文本内容,用于追加语音识别结果
  String _baseText = '';

  @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: [
      Consumer<ApplicationState>(builder: (context, appState, _) {
        return FutureBuilder<Baby>(
            future: appState.getBabyData(),
            builder: (BuildContext context, AsyncSnapshot<Baby> snapshot) {
              if (!snapshot.hasData) {
                return const Center(
                  child: CircularProgressIndicator(),
                );
              } else {
                return MyForm(
                  baby: snapshot.requireData,
                  descriptionController: _descriptionController,
                  stopListening: stopListening,
                );
              }
            });
      }),
      MicrophoneWidget(speech.isNotListening, startListening, stopListening),
    ]);
  }

  void startListening() {
    // 记录当前输入框的文本作为基础,后续语音内容追加至此
    _baseText = _descriptionController.text;
    speech.listen(
      onResult: resultListener,
      // 使用dictation模式支持连续语音输入
      listenMode: ListenMode.dictation,
    );
  }

  void stopListening() {
    speech.stop();
  }

  void resultListener(SpeechRecognitionResult result) {
    setState(() {
      lastWords = result.recognizedWords;
      // 合并基础文本与语音识别结果,去除多余空格
      _descriptionController.text = '$_baseText $lastWords'.trim();
    });
  }
}

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.descriptionController,
    required this.stopListening,
  });

  final Baby baby;
  final TextEditingController descriptionController;
  final void Function() stopListening;

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>(debugLabel: 'MyFormState');

  @override
  void initState() {
    super.initState();
    // 初始化输入框为Firestore中存储的描述内容
    if (widget.baby.description != null) {
      widget.descriptionController.text = widget.baby.description!;
    }
  }

  void _saveBabyProfile(appState) async {
    widget.stopListening();
    if (_formKey.currentState!.validate()) {
      widget.baby.description = widget.descriptionController.text;
      appState.setBabyData(widget.baby);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
            controller: widget.descriptionController,
            decoration: const InputDecoration(labelText: '描述'),
          ),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => ElevatedButton(
              onPressed: () => _saveBabyProfile(appState),
              child: const Text("保存"),
            ),
          ),
        ],
      ),
    );
  }
}

关键修改说明

  1. 语音输入追加逻辑:

    • 添加_baseText变量,每次启动语音时记录当前输入框的文本内容。
    • 语音识别结果实时追加到_baseText后,避免覆盖手动编辑的内容。
    • 使用ListenMode.dictation模式,支持连续自然的语音输入。
  2. 移除错误的文本重置逻辑:

    • 删除MyForm中build方法内的文本重置代码,改为在initState中初始化输入框内容,避免重复覆盖。
  3. 简化状态管理:

    • 移除冗余的textChanged变量及相关方法,减少状态冲突点。

测试场景验证

修改后可完美支持你提到的两种交互序列:

  1. 开启麦克风→语音输入→编辑文本→再次语音输入:语音内容会追加到编辑后的文本末尾。
  2. 开启麦克风→语音输入→关闭麦克风→编辑文本→重新开启麦克风输入:同样基于编辑后的内容追加语音结果。

内容的提问来源于stack exchange,提问作者Giulia Santoiemma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:54:54