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

89 views Asked by At

'm currently facing an issue with ReorderableListView.builder while using the BloC pattern in Flutter.

When I try to update the list, which is stored in my Bloc State, through an event in the onReorder callback of ReorderableListView.builder, the list item doesn't visually reorder instantly. Instead, it stays in its original position for a brief moment and then jumps to its reordered position. This gives a not-so-pleasant user experience.

Here's the code where I tried to solely rely on the BloC state without using a 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,
                    ),
                  );
            },
          );
        },
      ),
    );
  }
}

To work around this issue, I've created a local list (localList) that gets updated immediately and then sends an event to the Bloc to update its state. This approach works, but it makes the code look cluttered.

Here's the code where I used a 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,
                          ),
                        );
                  });
                },
              );
            },
          );
        },
      ),
    );
  }
}

Given the scenarios above, is there a more elegant solution to this problem, or is this the recommended way when working with the BloC pattern?

==== Update with Additional Information:

I'd like to provide some additional context to my original question. Here is the segment of my Bloc where I handle the reorder event using the 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
  );
});

To better assist in troubleshooting and for a more comprehensive view, I've created a minimum reproducible code that replicates the behavior I've described. You can access it on GitHub via this link. if you run the provided minimum reproducible code on the web, you'll notice that when you try to change the order of an item, it initially reverts back to its original position, and then quickly snaps to its new intended position.

Any insights or suggestions would be highly appreciated!

1

There are 1 answers

1
cyberail On

I noticed you are using buildWhen

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

Are you sure your editinForm.pages are different objects every time, you receive the state?

Are you ware that they need to be different objects, most of the time when we emit new state to make it different, we do

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