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

Flutter状态管理:能否用Selector替代Consumer避免UI重建?

问题分析与解决方案

根本原因

你这段代码里,每次Consumer触发UI重建时,都会先清空textControllers再重新创建所有输入控制器,还每次都把textControllers[index].text设为Provider里的初始_cale.description——这就导致一重建,用户刚输入的内容就被覆盖回初始值了。而点击输入框外部触发重建,多半是因为你用Consumer监听了整个CalendarViewProvider,只要Provider里任何状态变了,整个列表都会刷新。

Selector能不能解决?

Selector确实能替代Consumer,它能让你精准监听Provider里特定的状态部分,只有当你指定的状态变化时才触发builder重建,减少没必要的UI刷新。但光换Selector还不够,还得把TextEditingController的管理逻辑给修好,不然哪怕重建次数少了,只要一重建,输入内容还是会被覆盖。

完整解决方案

1. 先把输入控制器的管理逻辑改对

别在builder方法里每次都清空重建控制器,应该在初始化或者列表长度变化的时候才调整控制器数量,而且只在创建控制器的时候设置初始值,之后别每次builder都赋值,不然用户输入的内容肯定会被冲掉。

2. 用Selector替代Consumer

通过Selector只监听影响UI的关键状态——比如列表的长度、每个item的code/start/title这些固定字段,至于description,因为是本地编辑的内容,就不用监听Provider里的了,直接用输入控制器维护就行。这样只有当列表新增/删除、或者固定字段变化时才会触发重建,日常输入的时候不会随便刷新。

修改后的代码示例

Form(
  key: _calendarFormKey,
  child: Selector<CalendarViewProvider, List<CalendarItem>>(
    // 只监听列表的结构和固定字段变化,忽略description(本地用controller维护)
    selector: (context, cal) => cal.map((item) => CalendarItem(
      code: item.code,
      start: item.start,
      end: item.end,
      title: item.title,
      description: '', // 这里不需要监听,所以随便填个占位符就行
    )).toList(),
    builder: (context, calList, child) {
      // 只有当列表长度变化时,才调整控制器数量
      if (textControllers.length != calList.length) {
        textControllers.clear();
        for (int i = 0; i < calList.length; i++) {
          final controller = TextEditingController();
          // 只在创建控制器时设置初始值
          controller.text = calList[i].description ?? '';
          textControllers.add(controller);
        }
      }

      if (calList.isEmpty) {
        return Center(child: Text('List is empty'));
      }

      return ListView.separated(
        separatorBuilder: (context, index) => SizedBox(height: 10),
        itemCount: calList.length,
        itemBuilder: (context, index) {
          final _cale = calList[index];

          void clearController() {
            context.read<CalendarViewProvider>().removeCalendar(code: _cale.code);
            textControllers[index].clear();
          }

          void addToList() {
            caleList.clear();
            for (int i = 0; i < calList.length; i++) {
              final _cal = calList[i];
              final caleData = {
                'code': _cal.code,
                'start': _cal.start,
                'end': _cal.end,
                'title': _cal.title,
                'description': textControllers[i].text,
              };
              caleList.add(caleData);
            }
          }

          return Container(
            height: ScreenConfig.screenHeight * .08,
            child: Row(
              children: [
                SizedBox(width: 3),
                Text(
                  _cale.start?.substring(11, 16) ?? '',
                  style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[600]),
                ),
                Stack(
                  children: [
                    Image.asset(
                      AppImages.dietDayImg,
                      height: ScreenConfig.screenHeight * .15,
                      width: ScreenConfig.screenWidth * .21,
                    ),
                    Positioned.fill(
                      child: Center(
                        child: Text(
                          _cale.title?.substring(0, 1) ?? '',
                          style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14),
                        ),
                      ),
                    ),
                  ],
                ),
                SizedBox(width: 1),
                SingleChildScrollView(
                  child: Container(
                    height: ScreenConfig.screenHeight * .078,
                    width: ScreenConfig.screenWidth * .55,
                    decoration: BoxDecoration(
                      color: Color(0xFFd6e7ea),
                      borderRadius: BorderRadius.all(Radius.circular(10)),
                    ),
                    child: Padding(
                      padding: const EdgeInsets.only(left: 5),
                      child: TextFormField(
                        controller: textControllers[index],
                        maxLines: 20,
                        decoration: InputDecoration(border: InputBorder.none),
                        onSaved: (newValue) {
                          if (newValue != null) {
                            textControllers[index].text = newValue;
                          }
                          addToList();
                        },
                        onChanged: (value) {
                          caleList[index]['description'] = value;
                          addToList();
                        },
                      ),
                    ),
                  ),
                ),
                IconButton(
                  onPressed: deleteDialog,
                  icon: Icon(Icons.delete),
                  iconSize: 12,
                  color: Colors.red,
                ),
              ],
            ),
          );
        },
      );
    },
  ),
)

关键修改点说明

  • Selector的作用:通过selector函数只提取需要监听的字段,这样只有列表新增/删除、或者这些固定字段变化时才会触发重建,日常输入操作不会触发刷新。
  • 控制器管理:只在列表长度变化时才创建/调整控制器,而且只在创建时设置初始值,之后不再赋值,彻底避免用户输入被覆盖。
  • 简化逻辑:把addToList的冗余参数去掉了,直接用控制器的当前值,代码更简洁。

额外提醒

  • 页面销毁的时候记得销毁所有输入控制器,防止内存泄漏:
@override
void dispose() {
  for (var controller in textControllers) {
    controller.dispose();
  }
  super.dispose();
}
  • 如果不需要把编辑中的description同步到Provider,就完全用输入控制器维护状态,提交的时候再统一收集所有数据就行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 20:58:09