Flutter中使用Bloc处理ReorderableList重排时项闪烁问题
问题:Flutter Web端使用Bloc管理可重排列表时重排出现闪烁
我开发可重排列表时遇到重排闪烁问题:仅在Bloc管理状态时出现,用setState则流畅无闪烁;且仅Web端复现,Mac端Bloc运行完全正常。Flutter 3.10.0、3.7.12版本均存在此问题,尝试watch、select、BlocBuilder获取数据,问题依旧。
复现代码
import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; void main() { runApp( MaterialApp( home: BlocProvider( create: (context) => MainBloc(), child: const MyHomePage(), ), ), ); } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { final items = context.select((MainBloc bloc) => bloc.state.items); return Scaffold( body: Center( child: ReorderableList( itemBuilder: (context, index) { final value = items[index]; return _ListItem( key: ValueKey(index), // 问题根源之一 index: index, value: value, ); }, itemCount: items.length, onReorderStart: (index) {}, onReorderEnd: (index) {}, onReorder: (oldIndex, newIndex) { context.read<MainBloc>().add(ItemsReordered(oldIndex, newIndex)); }, ), ), ); } } class _ListItem extends StatelessWidget { const _ListItem({ required super.key, required this.index, required this.value, }); final int index; final String value; @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ ReorderableDragStartListener( index: index, child: const MouseRegion( cursor: SystemMouseCursors.grab, child: Padding( padding: EdgeInsets.all(12), child: Icon( Icons.drag_indicator, color: Colors.grey, ), ), ), ), Expanded( flex: 7, child: TextField( enabled: false, readOnly: true, decoration: const InputDecoration( disabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey), ), ), controller: TextEditingController(text: value), ), ), const SizedBox(width: 12), ], ), ), ); } } class MainBloc extends Bloc<ItemsReordered, MainState> { MainBloc() : super(const MainState()) { on<ItemsReordered>(_onItemsReordered); } void _onItemsReordered( ItemsReordered event, Emitter<MainState> emit, ) { final oldIndex = event.oldIndex; final newIndex = event.newIndex; final updatedNewIndex = oldIndex < newIndex ? newIndex - 1 : newIndex; final items = [...state.items]; final element = items.removeAt(oldIndex); items.insert(updatedNewIndex, element); emit( state.copyWith(items: items), ); } } class MainState extends Equatable { const MainState({ this.items = const [ '1 - This is the first item', '2 - Second item here', '3 - Maybe the third item?', ], }); final List<String> items; @override List<Object> get props => [items]; MainState copyWith({List<String>? items}) { return MainState(items: items ?? this.items); } } class ItemsReordered extends Equatable { const ItemsReordered(this.oldIndex, this.newIndex); final int oldIndex; final int newIndex; @override List<Object> get props => [oldIndex, newIndex]; }
问题分析
- Key的错误使用:用
ValueKey(index)作为列表项的Key,当列表重排时,所有位置变化的项的Key都会改变。Flutter会认为这些是全新的组件,销毁旧组件并重建新组件,Web端DOM渲染对这种重建更敏感,因此出现闪烁;Mac端原生渲染优化较好,无明显感知。 - Bloc状态更新的叠加影响:Bloc状态更新会触发组件树重建,结合基于索引的Key,进一步放大了组件重建的范围,加剧了闪烁问题。
修复方案
1. 使用基于数据本身的稳定Key
将列表项的Key从ValueKey(index)改为ValueKey(value)(示例中每个value唯一,适合作为Key),确保列表项在重排后Key保持稳定,Flutter可以复用原有组件,避免不必要的重建。
修改后的itemBuilder部分:
itemBuilder: (context, index) { final value = items[index]; return _ListItem( key: ValueKey(value), // 改为基于数据的Key index: index, value: value, ); },
2. (进阶)为列表项添加唯一ID(可选)
如果列表项内容可能重复,建议给每个项添加唯一ID字段,比如定义一个ListItemModel类:
class ListItemModel extends Equatable { final String id; final String text; const ListItemModel({required this.id, required this.text}); @override List<Object> get props => [id, text]; }
然后将MainState中的items改为List<ListItemModel>,Key使用ValueKey(item.id),这样无论内容是否重复,Key始终稳定。
验证效果
修改后,Web端使用Bloc管理状态时,重排列表项不再出现闪烁,和setState的表现一致,Mac端也保持正常运行。
内容的提问来源于stack exchange,提问作者jsgalarraga
相关产品推荐
相关产品推荐

