Flutter中Bloc模式下ReorderableListView.builder排序视觉延迟问题
Flutter Bloc模式下ReorderableListView排序视觉延迟问题优化
问题描述
在Flutter中使用Bloc模式管理状态时,使用ReorderableListView.builder进行列表排序,触发onReorder回调后通过发送Bloc事件更新状态中的列表,会出现列表项短暂停留在原位置后再跳转到新位置的现象,用户体验较差。
原始实现(仅依赖Bloc状态)
仅通过Bloc状态驱动UI,未维护本地列表,导致视觉反馈延迟:
class ReorderProblem extends StatefulWidget { final String pageId; const ReorderProblem({Key? key, required this.pageId}) : super(key: key); @override State<ReorderProblem> createState() => _ReorderProblemState(); } class _ReorderProblemState extends State<ReorderProblem> { @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), child: BlocBuilder<FormatEditorPageBloc, FormatEditorPageState>( buildWhen: (previous, current) { return previous.editingForm.pages[widget.pageId] != current.editingForm.pages[widget.pageId]; }, builder: (context, state) { final page = state.editingForm.pages[widget.pageId]; if (page == null) return const Center(child: Text('Page not found')); return ReorderableListView.builder( buildDefaultDragHandles: false, itemBuilder: (context, index) { return ListTile( key: ValueKey(page.blocks[index].id), leading: ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle), ), trailing: IconButton( onPressed: () { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.addBlockAt( pageId: page.id, index: index + 1, newBlock: null, ), ); }, icon: const Icon(Icons.add), ), title: DeltaBlockView( deltaBlock: page.blocks[index], onDeltaBlockUpdated: (deltaBlock) { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.updateBlockAt( pageId: page.id, index: index, updatedBlock: deltaBlock, ), ); }, )); }, itemCount: page.blocks.length, onReorder: (oldIndex, newIndex) { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.reorderBlocks( pageId: page.id, oldIndex: oldIndex, newIndex: newIndex, ), ); }, ); }, ), ); } }
临时解决方案(本地列表同步)
通过维护本地列表localList,先更新本地列表实现即时视觉反馈,再发送Bloc事件同步状态,解决了延迟问题但代码冗余:
class ReorderProblem extends StatefulWidget { final String pageId; const ReorderProblem({Key? key, required this.pageId}) : super(key: key); @override State<ReorderProblem> createState() => _ReorderProblemState(); } class _ReorderProblemState extends State<ReorderProblem> { @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), child: BlocBuilder<FormatEditorPageBloc, FormatEditorPageState>( buildWhen: (previous, current) { return previous.editingForm.pages[widget.pageId] != current.editingForm.pages[widget.pageId]; }, builder: (context, state) { final page = state.editingForm.pages[widget.pageId]; if (page == null) return const Center(child: Text('Page not found')); return HookBuilder( builder: (context) { final localList = useState(page.blocks); useEffect(() { localList.value = page.blocks; return null; }, [page.blocks]); return ReorderableListView.builder( buildDefaultDragHandles: false, itemBuilder: (context, index) { return ListTile( key: ValueKey(localList.value[index].id), leading: ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle), ), trailing: IconButton( onPressed: () { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.addBlockAt( pageId: page.id, index: index + 1, newBlock: null, ), ); }, icon: const Icon(Icons.add), ), title: DeltaBlockView( deltaBlock: localList.value[index], onDeltaBlockUpdated: (deltaBlock) { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.updateBlockAt( pageId: page.id, index: index, updatedBlock: deltaBlock, ), ); }, )); }, itemCount: localList.value.length, onReorder: (oldIndex, newIndex) { setState(() { final blockToMove = localList.value[oldIndex]; if (oldIndex < newIndex) { localList.value = localList.value .removeAt(oldIndex) .insert(newIndex - 1, blockToMove) .toIList(); } else { localList.value = localList.value .removeAt(oldIndex) .insert(newIndex, blockToMove) .toIList(); } context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.reorderBlocks( pageId: page.id, oldIndex: oldIndex, newIndex: newIndex, ), ); }); }, ); }, ); }, ), ); } }
Bloc事件处理代码
Bloc中处理排序事件的核心逻辑:
on<FormatEditorPageDeltaPageEvent>((event, emit) { event.map(reorderBlocks: (value) { final blocks = state.editingForm.pages[value.pageId]!.blocks; final blockToMove = blocks[value.oldIndex]; if (value.oldIndex < value.newIndex) { emit(state.copyWith.editingForm( pages: state.editingForm.pages.update( value.pageId, (page) => page.copyWith( blocks: blocks .removeAt(value.oldIndex) .insert(value.newIndex - 1, blockToMove) .toIList(), ), ), )); } else { emit(state.copyWith.editingForm( pages: state.editingForm.pages.update( value.pageId, (page) => page.copyWith( blocks: blocks .removeAt(value.oldIndex) .insert(value.newIndex, blockToMove) .toIList(), ), ), )); } }, // 其他事件处理逻辑 ); });
优化方案推荐
1. 简化本地临时状态维护
无需使用Hook,直接在State中维护临时列表,仅在拖动时临时更新,Bloc状态更新后自动同步,兼顾即时反馈和代码简洁:
import 'package:flutter/foundation.dart'; // 引入listEquals class ReorderProblem extends StatefulWidget { final String pageId; const ReorderProblem({Key? key, required this.pageId}) : super(key: key); @override State<ReorderProblem> createState() => _ReorderProblemState(); } class _ReorderProblemState extends State<ReorderProblem> { List<DeltaBlock>? _localBlocks; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: BlocBuilder<FormatEditorPageBloc, FormatEditorPageState>( buildWhen: (previous, current) { return previous.editingForm.pages[widget.pageId] != current.editingForm.pages[widget.pageId]; }, builder: (context, state) { final page = state.editingForm.pages[widget.pageId]; if (page == null) return const Center(child: Text('Page not found')); // 同步Bloc状态到本地临时列表 if (_localBlocks == null || !listEquals(_localBlocks, page.blocks)) { _localBlocks = List.from(page.blocks); } return ReorderableListView.builder( buildDefaultDragHandles: false, itemBuilder: (context, index) { final block = _localBlocks![index]; return ListTile( key: ValueKey(block.id), leading: ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle), ), trailing: IconButton( onPressed: () { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.addBlockAt( pageId: page.id, index: index + 1, newBlock: null, ), ); }, icon: const Icon(Icons.add), ), title: DeltaBlockView( deltaBlock: block, onDeltaBlockUpdated: (deltaBlock) { context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.updateBlockAt( pageId: page.id, index: index, updatedBlock: deltaBlock, ), ); }, ), ); }, itemCount: _localBlocks!.length, onReorder: (oldIndex, newIndex) { setState(() { // 临时更新本地列表,实现即时视觉反馈 final blockToMove = _localBlocks!.removeAt(oldIndex); final insertIndex = oldIndex < newIndex ? newIndex - 1 : newIndex; _localBlocks!.insert(insertIndex, blockToMove); }); // 发送Bloc事件同步状态 context.read<FormatEditorPageBloc>().add( FormatEditorPageDeltaPageEvent.reorderBlocks( pageId: page.id, oldIndex: oldIndex, newIndex: newIndex, ), ); }, ); }, ), ); } }
2. 优化Bloc状态更新效率
确保状态拷贝操作尽可能轻量化,避免不必要的深层拷贝。例如检查editingForm.pages.update是否可以通过更高效的方式生成新状态,减少Widget重建的耗时。
3. 保证状态一致性
如果存在其他渠道修改Bloc中列表的情况,可配合BlocListener监听状态变化,强制同步本地临时列表,避免出现状态不一致:
BlocListener<FormatEditorPageBloc, FormatEditorPageState>( listener: (context, state) { final page = state.editingForm.pages[widget.pageId]; if (page != null) { setState(() { _localBlocks = List.from(page.blocks); }); } }, child: // ... ReorderableListView代码 )
方案说明
这种本地临时状态+Bloc状态同步的方式是Bloc模式下处理即时视觉反馈场景的常用方案,并非不推荐,通过优化可以大幅减少代码冗余,同时兼顾用户体验和状态管理的规范性。
内容的提问来源于stack exchange,提问作者ywin s
相关产品推荐
相关产品推荐

