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

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];
}

问题分析

  1. Key的错误使用:用ValueKey(index)作为列表项的Key,当列表重排时,所有位置变化的项的Key都会改变。Flutter会认为这些是全新的组件,销毁旧组件并重建新组件,Web端DOM渲染对这种重建更敏感,因此出现闪烁;Mac端原生渲染优化较好,无明显感知。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 13:45:04