Flutter:在 ReorderableListView.builder 中从 Bloc State 重新排序列表的问题

Flutter: Issue with Reordering List from Bloc State in ReorderableListView.builder

提问人:ywin s 提问时间:10/23/2023 最后编辑:ywin s 更新时间:10/23/2023 访问量:34

问:

目前在 Flutter 中使用 BloC 模式时遇到了 ReorderableListView.builder 的问题。

当我尝试通过 ReorderableListView.builder 的 onReorder 回调中的事件更新存储在我的 Bloc State 中的列表时,列表项不会立即直观地重新排序。相反,它会在原来的位置上停留一小会儿,然后跳到重新排序的位置。这给人一种不太愉快的用户体验。

这是我尝试完全依赖 BloC 状态而不使用 localList 的代码:

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) {
    // final theme = Theme.of(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,
                            ),
                          );
                    },
                  )); //const DeltaBlockView());
            },
            itemCount: page.blocks.length,
            onReorder: (oldIndex, newIndex) {
              context.read<FormatEditorPageBloc>().add(
                    FormatEditorPageDeltaPageEvent.reorderBlocks(
                      pageId: page.id,
                      oldIndex: oldIndex,
                      newIndex: newIndex,
                    ),
                  );
            },
          );
        },
      ),
    );
  }
}

为了解决此问题,我创建了一个本地列表 (localList),该列表会立即更新,然后向 Bloc 发送一个事件以更新其状态。这种方法有效,但它使代码看起来杂乱无章。

这是我使用 localList 的代码:

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) {
    // final theme = Theme.of(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);
              // Listen for external changes to page.blocks and update localList
              useEffect(() {
                localList.value = page.blocks;
                return null; // This represents the cleanup function, which we don't need here.
              }, [
                page.blocks
              ]); // This dependency list ensures useEffect runs whenever page.blocks changes.

              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,
                                ),
                              );
                        },
                      )); //const DeltaBlockView());
                },
                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 的片段,我使用 FormatEditorPageDeltaPageEvent 处理重新排序事件:

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(),
          ),
        ),
      ));
    }
  },
  // other codes
  );
});

为了更好地帮助进行故障排除和更全面的视图,我创建了一个最小可重现的代码来复制我所描述的行为。您可以通过此链接在 GitHub 上访问它。 如果您在 Web 上运行提供的最小可重现代码,您会注意到,当您尝试更改项目的顺序时,它最初会恢复到其原始位置,然后迅速捕捉到其新的预期位置。

任何见解或建议将不胜感激!

flutter bloc flutter-reorderable-list查看

评论

0赞 cyberail 10/23/2023
删除相关的 bloc 方法代码,并向我们展示您的状态类。

答:

0赞 cyberail 10/23/2023 #1

我注意到你正在使用buildWhen

buildWhen: (previous, current) {
          return previous.editingForm.pages[widget.pageId] !=
              current.editingForm.pages[widget.pageId];
        },

你确定你每次都是不同的对象,你收到状态吗?editinForm.pages

你是否知道它们需要是不同的对象,大多数时候,当我们发出新的状态来使其不同时,我们确实这样做了

emit(state.copyWith(....))

评论

0赞 ywin s 10/23/2023
非常感谢您抽出宝贵时间提供反馈。我衷心感谢您对细节的关注。是的,在我的 Bloc 中,我确实使用 state.copyWith(...) 方法发出了一个新状态。我已经考虑了您的反馈,并用其他信息更新了我的答案,包括集团的相关部分。请查看更新的部分。