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
相关产品推荐
相关产品推荐

