如何阻止Bloc刷新列表中的所有项?
Bloc 可展开列表全量刷新问题解决
我正在学习Bloc,但操作上出了问题——每次触发事件时,列表里的所有项都会刷新!
这是一个可展开视图列表,点击列表项时它会改变尺寸并展开。
原代码情况
Event类
class HelpEvents {} class ExpandView extends HelpEvents {} class CollapseView extends HelpEvents {}
State类
class HelpStates {} class ToggleViewState extends HelpStates {}
Bloc类
class HelpScreenBloc extends Bloc<HelpEvents, HelpStates> { HelpScreenBloc() : super(HelpStates()){ //InitialState /// When user press Expand on<ExpandView>((event, emit) async { emit(ToggleViewState()); //first state }); /// When user press Collapse on<CollapseView>((event, emit) { emit(ToggleViewState()); //collapsed state }); } }
列表项代码
bool isExpanded = false; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20), child: Material( elevation: appElevation, child: BlocBuilder<HelpScreenBloc, HelpStates>( builder: (context, state) { print("This is supposed to run Once"); return InkWell( onTap: () { // <-- Trigger Event ---------------- here if(!isExpanded) { context.read<HelpScreenBloc>().add(ExpandView()); } else { context.read<HelpScreenBloc>().add(CollapseView()); } isExpanded = !isExpanded; }, child: Expanded( child: AnimatedContainer( height: isExpanded ? 150 : 70, duration: const Duration(milliseconds: 180), child: const Text("text"), ), ), ); } ) ), ); }
我还发现用State变量会改变列表中的所有项!这就是为什么我被迫在Widget中使用全局bool变量isExpanded,但我觉得这不太规范,应该在State类中操作变量并通过state.variable引用。
问题根源
- Bloc状态无项区分:当前
ToggleViewState没有携带任何列表项的唯一标识,所有列表项的BlocBuilder都会在状态更新时强制重建,导致全量刷新。 - 状态管理不独立:全局
isExpanded是所有列表项共用的变量,修改它会同步影响所有项;就算用StatefulWidget的本地状态,若列表项未做隔离复用,也会出现批量修改的问题。
修正方案
要让每个列表项的展开状态独立管理,需要让Bloc的状态记录每个项的标识对应的展开状态,具体步骤如下:
1. 修改Event类,添加项唯一标识
让展开/折叠事件携带列表项ID,让Bloc能定位到具体操作的项:
abstract class HelpEvents {} class ExpandView extends HelpEvents { final String itemId; // 列表项唯一标识 ExpandView(this.itemId); } class CollapseView extends HelpEvents { final String itemId; CollapseView(this.itemId); }
2. 修改State类,存储多状态映射
用Map<String, bool>记录每个itemId对应的展开状态,同时提供复制方法用于状态更新:
class HelpState { final Map<String, bool> expandedStates; // key: itemId, value: 是否展开 HelpState({required this.expandedStates}); // 复制现有状态并更新指定项,避免直接修改原状态 HelpState copyWith({Map<String, bool>? expandedStates}) { return HelpState( expandedStates: expandedStates ?? this.expandedStates, ); } }
3. 修改Bloc类,处理带标识的事件
初始化时创建空状态映射,根据事件中的itemId更新对应项的状态:
class HelpScreenBloc extends Bloc<HelpEvents, HelpState> { HelpScreenBloc() : super(HelpState(expandedStates: {})) { on<ExpandView>((event, emit) { final newStates = Map<String, bool>.from(state.expandedStates); newStates[event.itemId] = true; emit(state.copyWith(expandedStates: newStates)); }); on<CollapseView>((event, emit) { final newStates = Map<String, bool>.from(state.expandedStates); newStates[event.itemId] = false; emit(state.copyWith(expandedStates: newStates)); }); } }
4. 修改列表项Widget,从Bloc状态获取状态
移除全局isExpanded,通过当前项ID从Bloc状态中读取展开状态,点击时发送带ID的事件:
class HelpListItem extends StatelessWidget { final String itemId; final double appElevation; const HelpListItem({super.key, required this.itemId, required this.appElevation}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20), child: Material( elevation: appElevation, child: BlocBuilder<HelpScreenBloc, HelpState>( // 可选优化:只有当前项状态变化时才重建 buildWhen: (previous, current) { return previous.expandedStates[itemId] != current.expandedStates[itemId]; }, builder: (context, state) { // 默认未展开 final isExpanded = state.expandedStates[itemId] ?? false; return InkWell( onTap: () { if (!isExpanded) { context.read<HelpScreenBloc>().add(ExpandView(itemId)); } else { context.read<HelpScreenBloc>().add(CollapseView(itemId)); } }, child: AnimatedContainer( height: isExpanded ? 150 : 70, duration: const Duration(milliseconds: 180), child: const Text("text"), ), ); }, ), ), ); } }
这样修改后,每个列表项的展开状态会独立存储在Bloc的State中,点击某一项只会更新对应项的状态,其他列表项不会触发重建,既解决了全量刷新问题,也符合Bloc的状态管理规范。
内容的提问来源于stack exchange,提问作者Ziad H.
相关产品推荐
相关产品推荐

