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

Flutter填空题APP:页面切换后TextField输入内容丢失的解决方法

问题分析

输入内容丢失的核心原因有两点:

  1. PageView 默认会销毁当前视窗外的页面,切换回来时会重建 TextWithBlanks 和 BlankWord,之前的输入值因存储在子组件内部而被重置。
  2. 当前输入值直接存在 BlankWord 的 widget 属性中,违反 Flutter 状态管理原则,且无法在组件重建后保留数据。
解决方案

通过状态提升将所有输入数据统一管理在父组件,并结合页面状态保持、文本控制器实现输入值持久化,具体步骤如下:


1. 父组件统一管理填空数据

在 _PlayGameState 中创建数据结构,存储每个题目下所有填空的答案、用户输入和正确状态:

class PlayGame extends StatefulWidget {
  final List document;
  PlayGame({Key? key, required this.document}) : super(key: key);

  @override
  State<PlayGame> createState() => _PlayGameState();
}

class _PlayGameState extends State<PlayGame> {
  int quizCount = 0;
  // 存储每个题的填空数据:[题目索引][填空索引] -> {答案、用户输入、是否正确}
  List<List<Map<String, dynamic>>> quizAnswers = [];

  @override
  void initState() {
    super.initState();
    // 初始化数据结构,解析每个题目中的填空
    for (var text in widget.document) {
      final split = TextWithBlanks.regex.split(text);
      List<Map<String, dynamic>> blanks = [];
      for (var part in split) {
        if (part.startsWith('{')) {
          String answer = part.substring(1, part.length - 1);
          blanks.add({
            'answer': answer,
            'userInput': '',
            'isCorrect': false,
          });
        }
      }
      quizAnswers.add(blanks);
    }
  }

  // 更新指定题、指定填空的输入值
  void updateUserInput(int quizIndex, int blankIndex, String input) {
    setState(() {
      quizAnswers[quizIndex][blankIndex]['userInput'] = input;
      quizAnswers[quizIndex][blankIndex]['isCorrect'] = input == quizAnswers[quizIndex][blankIndex]['answer'];
    });
  }

  // 获取全部正确填空数量
  int getCorrectCount() {
    int count = 0;
    for (var quiz in quizAnswers) {
      for (var blank in quiz) {
        if (blank['isCorrect'] == true) count++;
      }
    }
    return count;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Center(
          child: Text(
            "$quizCount/5",
            style: const TextStyle(fontSize: 25, fontStyle: FontStyle.italic, fontWeight: FontWeight.bold),
          ),
        ),
        actions: [
          if (quizCount == 5)
            GestureDetector(
              onTap: () {
                Navigator.push(context, MaterialPageRoute(builder: (context) => Result()));
                print(getCorrectCount());
              },
              child: const Row(
                children: [
                  Text("採点する", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  Icon(Icons.arrow_forward, size: 20),
                  SizedBox(width: 10)
                ],
              ),
            )
          else
            const SizedBox.shrink()
        ],
        automaticallyImplyLeading: false,
      ),
      body: PageView(
        onPageChanged: (counter) => setState(() => quizCount = counter + 1),
        children: [
          QuizText(
            quizIndex: 0,
            text: widget.document[0],
            blanks: quizAnswers[0],
            onInputChanged: (i, input) => updateUserInput(0, i, input),
          ),
          QuizText(
            quizIndex: 1,
            text: widget.document[1],
            blanks: quizAnswers[1],
            onInputChanged: (i, input) => updateUserInput(1, i, input),
          ),
          QuizText(
            quizIndex: 2,
            text: widget.document[2],
            blanks: quizAnswers[2],
            onInputChanged: (i, input) => updateUserInput(2, i, input),
          ),
          QuizText(
            quizIndex: 3,
            text: widget.document[3],
            blanks: quizAnswers[3],
            onInputChanged: (i, input) => updateUserInput(3, i, input),
          ),
          QuizText(
            quizIndex: 4,
            text: widget.document[4],
            blanks: quizAnswers[4],
            onInputChanged: (i, input) => updateUserInput(4, i, input),
          ),
        ],
      ),
    );
  }
}

2. 保持PageView页面状态

给 QuizText 添加 AutomaticKeepAliveClientMixin,避免PageView销毁不可见页面:

class QuizText extends StatefulWidget {
  final int quizIndex;
  final String text;
  final List<Map<String, dynamic>> blanks;
  final Function(int, String) onInputChanged;

  const QuizText({
    Key? key,
    required this.quizIndex,
    required this.text,
    required this.blanks,
    required this.onInputChanged,
  }) : super(key: key);

  @override
  State<QuizText> createState() => _QuizTextState();
}

class _QuizTextState extends State<QuizText> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用super.build
    return Container(
      constraints: const BoxConstraints.expand(),
      child: Padding(
        padding: const EdgeInsets.all(5.0),
        child: Card(
          child: SizedBox(
            height: double.infinity,
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.only(bottom: 30.0),
                child: TextWithBlanks(
                  text: widget.text,
                  blanks: widget.blanks,
                  onInputChanged: widget.onInputChanged,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

3. 修改TextWithBlanks传递数据

让 TextWithBlanks 接收父组件的填空数据,并传递给每个 BlankWord:

class TextWithBlanks extends StatefulWidget {
  final String text;
  static final regex = RegExp("(?={)|(?<=})");
  final List<Map<String, dynamic>> blanks;
  final Function(int, String) onInputChanged;

  TextWithBlanks({
    Key? key,
    required this.text,
    required this.blanks,
    required this.onInputChanged,
  }) : super(key: key);

  @override
  State<TextWithBlanks> createState() => _TextWithBlanksState();
}

class _TextWithBlanksState extends State<TextWithBlanks> {
  @override
  Widget build(BuildContext context) {
    final split = widget.text.split(TextWithBlanks.regex);
    int blankIndex = 0;
    return Padding(
      padding: const EdgeInsets.only(top: 30.0, right: 30.0, left: 30.0),
      child: Text.rich(
        TextSpan(
          style: const TextStyle(fontSize: 15, height: 3.0),
          children: <InlineSpan>[
            for (String text in split)
              text.startsWith('{')
                  ? WidgetSpan(
                      child: BlankWord(
                        answer: widget.blanks[blankIndex]['answer'],
                        userInput: widget.blanks[blankIndex]['userInput'],
                        onChanged: (input) => widget.onInputChanged(blankIndex++, input),
                      ),
                    )
                  : TextSpan(text: text),
          ],
        ),
      ),
    );
  }
}

4. 用TextEditingController控制输入框

修改 BlankWord(类名首字母大写符合Dart规范),使用控制器同步输入值:

class BlankWord extends StatefulWidget {
  final String answer;
  final String userInput;
  final Function(String) onChanged;

  const BlankWord({
    Key? key,
    required this.answer,
    required this.userInput,
    required this.onChanged,
  }) : super(key: key);

  @override
  State<BlankWord> createState() => _BlankWordState();
}

class _BlankWordState extends State<BlankWord> {
  late TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.userInput);
  }

  @override
  void didUpdateWidget(covariant BlankWord oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 父组件数据更新时同步控制器文本
    if (oldWidget.userInput != widget.userInput) {
      _controller.text = widget.userInput;
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    double answerWidth = widget.answer.length * 15.0;
    return SizedBox(
      width: answerWidth,
      child: TextFormField(
        controller: _controller,
        maxLines: null,
        cursorColor: Colors.grey,
        style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
        autofocus: false,
        maxLength: widget.answer.length + 5,
        onChanged: widget.onChanged,
        decoration: const InputDecoration(
          counterText: "",
          hintStyle: TextStyle(color: Colors.grey, fontSize: 12),
        ).copyWith(hintText: widget.answer),
      ),
    );
  }
}

关键修改说明
  1. 状态提升:所有填空数据集中在父组件管理,确保页面切换时数据不丢失。
  2. 页面保活:通过 AutomaticKeepAliveClientMixin 避免PageView销毁页面,减少不必要的重建。
  3. 控制器同步:用 TextEditingController 绑定输入框,确保输入值与父组件数据实时同步。
  4. 单向数据流:子组件通过回调通知父组件更新数据,符合Flutter状态管理最佳实践。

内容的提问来源于stack exchange,提问作者wasa-bi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 15:18:04