'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!
I noticed you are using
buildWhen
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(....))