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

Flutter中Riverpod的StateProvider更新值错误 按钮状态相反如何修复

问题描述

在POC应用中使用riverpod实现状态管理,当前页面搭载文本编辑控制器,预期逻辑为:判断TextField输入文本是否为空,据此动态控制按钮的启用/禁用状态。实际运行时按钮状态始终与预期逻辑相反,需要修复。

原问题代码如下:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:to_do_list/models/task.dart';
import 'package:to_do_list/reusable_widgets/app_bar.dart';

class TaskScreen extends ConsumerStatefulWidget {
  const TaskScreen({Key? key}) : super(key: key);

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

class TaskScreenState extends ConsumerState<TaskScreen> {
  DateTime? _selectedDate;
  static final TextEditingController _titleController = TextEditingController();
  final TextEditingController _descriptionController = TextEditingController();
  var textProvider = StateProvider((_) => _titleController.text.isNotEmpty);

  @override
  Widget build(BuildContext context) {
    bool value = ref.watch(textProvider);
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: const AppBarWrapper(
        title: "To-Do List",
      ),
      body: Stack(children: [
        Padding(
          padding: const EdgeInsets.all(14.0),
          child: Column(
            children: [
              TextField(
                controller: _titleController,
                onChanged: (val) {
                  ref.read(textProvider.state).state = val.isEmpty;
                },
                autofocus: true,
                decoration: const InputDecoration(hintText: "Title"),
              ),
              const SizedBox(
                height: 14,
              ),
              TextField(
                controller: _descriptionController,
                decoration: const InputDecoration(hintText: "Description"),
                minLines: 3,
                maxLines: 5,
              ),
              SizedBox(
                width: 150,
                child: ListTile(
                  leading: const Icon(Icons.calendar_today),
                  title: Text(
                    _selectedDate?.toString() ?? 'No Date',
                  ),
                  onTap: () async {
                    _selectedDate = await showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime.now(),
                      lastDate: DateTime.now().add(const Duration(days: 365)),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
        Positioned(
          bottom: MediaQuery.of(context).viewInsets.bottom,
          left: 0,
          right: 0,
          child: ElevatedButton(
            onPressed: value
                ? () {
                    Navigator.of(context).pop(Task(
                        title: _titleController.text,
                        description: _descriptionController.text.isNotEmpty
                            ? _descriptionController.text
                            : null,
                        date: _selectedDate));
                  }
                : null,
            child: const Icon(
              Icons.done,
            ),
          ),
        ),
      ]),
    );
  }
}
问题原因

核心错误是布尔判断逻辑前后不一致:

  • textProvider初始化时,初始值设置为_titleController.text.isNotEmpty,即文本非空时状态为true,对应按钮可点击
  • 输入框onChanged回调中,给provider赋值写的是val.isEmpty,即文本为空时状态为true,对应按钮可点击
    两处逻辑完全反向,就会出现空文本时按钮可点、输入内容后按钮禁用的异常表现。

另外代码还有两个隐性问题:

  • _titleController被加了static修饰,页面销毁重建时控制器不会重置,会残留上次输入的内容
  • 日期选择逻辑拿到_selectedDate后没有调用setState,选完日期页面不会刷新显示新选中的日期
修复方法
  1. 修正onChanged中的赋值逻辑,和初始判断规则保持一致,将val.isEmpty改为val.isNotEmpty
  2. 去掉_titleController的static修饰,避免页面重建时的文本残留
  3. 在日期选择的await逻辑后添加setState,触发页面刷新显示选中的日期

修复后对应修改的关键代码段:

class TaskScreenState extends ConsumerState<TaskScreen> {
  DateTime? _selectedDate;
  // 去掉static修饰
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _descriptionController = TextEditingController();
  var textProvider = StateProvider((_) => _titleController.text.isNotEmpty);

  @override
  Widget build(BuildContext context) {
    bool value = ref.watch(textProvider);
    return Scaffold(
      // ...省略其他未改动代码
      body: Stack(children: [
        Padding(
          padding: const EdgeInsets.all(14.0),
          child: Column(
            children: [
              TextField(
                controller: _titleController,
                onChanged: (val) {
                  // 修正判断逻辑,和初始值规则统一
                  ref.read(textProvider.state).state = val.isNotEmpty;
                },
                autofocus: true,
                decoration: const InputDecoration(hintText: "Title"),
              ),
              // ...省略其他未改动组件
              SizedBox(
                width: 150,
                child: ListTile(
                  leading: const Icon(Icons.calendar_today),
                  title: Text(
                    _selectedDate?.toString() ?? 'No Date',
                  ),
                  onTap: () async {
                    _selectedDate = await showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime.now(),
                      lastDate: DateTime.now().add(const Duration(days: 365)),
                    );
                    // 补上setState刷新日期显示,加mounted判断避免组件销毁后报错
                    if (mounted) setState(() {});
                  },
                ),
              ),
            ],
          ),
        ),
        // ...省略未改动的按钮代码
      ]),
    );
  }
}

额外提示:如果页面里的provider只在当前State类使用,不建议把provider定义在State内部,最好放到全局作用域,避免页面重建时重复创建provider实例。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 09:57:29