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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 17:32:32