Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create a Drag-and-Drop Interface in FlutterFlow

FlutterFlow supports drag-and-drop through two Flutter patterns: ReorderableListView for sorting items within a single list, and Draggable with DragTarget for moving items between different containers like a Kanban board. Both require Custom Widgets. The critical rule is to only save the new order to Firestore when the drag ends — not during the drag.

What you'll learn

  • Using ReorderableListView to build a sortable to-do list
  • Building a Kanban board with Draggable and DragTarget widgets
  • Showing visual feedback (ghost image, drop target highlight) during drag
  • Saving the new order to Firestore only when the drag ends
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read45-60 minFlutterFlow Pro+ (Custom Widgets required)March 2026RapidDev Engineering Team
TL;DR

FlutterFlow supports drag-and-drop through two Flutter patterns: ReorderableListView for sorting items within a single list, and Draggable with DragTarget for moving items between different containers like a Kanban board. Both require Custom Widgets. The critical rule is to only save the new order to Firestore when the drag ends — not during the drag.

Two drag-and-drop patterns — which one to use

Flutter offers two distinct drag-and-drop widgets. ReorderableListView is the simplest: it handles all drag logic internally and calls onReorder when the user lifts their finger, giving you the old and new index. It's perfect for sorting a to-do list, playlist, or ranked items within one list. Draggable and DragTarget are lower-level and more flexible: any widget can be dragged, and you define which areas accept drops. Use this pattern for Kanban boards (drag tasks between status columns), file drag-to-folder interfaces, or any interaction where items move between distinct zones. Both patterns require Custom Widgets in FlutterFlow, which need the Pro plan.

Prerequisites

  • FlutterFlow Pro plan for Custom Widgets
  • FlutterFlow project with Firebase Firestore connected
  • A collection in Firestore for your draggable items (e.g., tasks with a 'sortOrder' field)
  • Basic understanding of FlutterFlow's Custom Code section

Step-by-step guide

1

Create a sortable list with ReorderableListView

Create a new Custom Widget named 'SortableTaskList'. It accepts a list of task maps (each with id, title, and sortOrder fields) and a callback function to save the new order. The widget displays a ReorderableListView. When the user drags an item to a new position, onReorder fires with the old and new index. Rearrange the local list, then call the save callback — not during the drag. Paste the code below into your Custom Widget editor. Add 'tasks' (JSON list) and 'onReorderComplete' (Action) as widget parameters.

sortable_task_list_widget.dart
1// Custom Widget: SortableTaskList
2import 'package:flutter/material.dart';
3
4class SortableTaskList extends StatefulWidget {
5 const SortableTaskList({
6 super.key,
7 required this.tasks,
8 required this.onReorderComplete,
9 });
10
11 final List<dynamic> tasks;
12 final Future<dynamic> Function(List<dynamic>) onReorderComplete;
13
14 @override
15 State<SortableTaskList> createState() => _SortableTaskListState();
16}
17
18class _SortableTaskListState extends State<SortableTaskList> {
19 late List<dynamic> _items;
20
21 @override
22 void initState() {
23 super.initState();
24 _items = List.from(widget.tasks);
25 }
26
27 void _onReorder(int oldIndex, int newIndex) {
28 setState(() {
29 if (newIndex > oldIndex) newIndex--;
30 final item = _items.removeAt(oldIndex);
31 _items.insert(newIndex, item);
32 });
33 // Only write to Firestore AFTER reorder is complete
34 widget.onReorderComplete(_items);
35 }
36
37 @override
38 Widget build(BuildContext context) {
39 return ReorderableListView(
40 onReorder: _onReorder,
41 children: [
42 for (int i = 0; i < _items.length; i++)
43 ListTile(
44 key: ValueKey(_items[i]['id']),
45 title: Text(_items[i]['title'] ?? ''),
46 trailing: const Icon(Icons.drag_handle),
47 ),
48 ],
49 );
50 }
51}

Expected result: A list where tasks can be dragged up and down to reorder. The onReorderComplete callback fires once when the drag ends.

2

Create a Cloud Function to save the new sort order

When onReorderComplete fires, you need to update sortOrder on every task document. A Firestore batch write is the efficient way — it updates all documents atomically in one network round trip. Create a Cloud Function named 'updateTaskOrder'. It accepts an array of task IDs in their new order and writes the sortOrder index (0, 1, 2...) back to each Firestore document. Call this from a FlutterFlow Custom Action that wraps the HTTP call.

functions/index.js
1// functions/index.js
2exports.updateTaskOrder = functions.https.onCall(async (data, context) => {
3 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
4
5 const { taskIds } = data; // array of Firestore doc IDs in new order
6 if (!Array.isArray(taskIds) || taskIds.length === 0) {
7 throw new functions.https.HttpsError('invalid-argument', 'taskIds must be a non-empty array');
8 }
9
10 const db = admin.firestore();
11 const batch = db.batch();
12
13 taskIds.forEach((id, index) => {
14 const ref = db.collection('tasks').doc(id);
15 batch.update(ref, { sortOrder: index });
16 });
17
18 await batch.commit();
19 return { success: true };
20});

Expected result: Cloud Function deployed. After a reorder, all affected task documents have updated sortOrder values in Firestore within 500ms.

3

Build a Kanban board with Draggable and DragTarget

For a Kanban board where tasks move between status columns (Todo, In Progress, Done), use Draggable and DragTarget. Create a Custom Widget named 'KanbanBoard'. Each column is a DragTarget. Each task card is a Draggable that carries the task ID as its data. When a task is dropped on a column, onAcceptWithDetails fires with the dragged task ID and the column's status. The widget updates the local state immediately for instant visual feedback, then calls the save callback.

kanban_board_widget.dart
1// Custom Widget: KanbanBoard (simplified single-column DragTarget)
2import 'package:flutter/material.dart';
3
4class KanbanColumn extends StatefulWidget {
5 const KanbanColumn({
6 super.key,
7 required this.status,
8 required this.tasks,
9 required this.onTaskMoved,
10 });
11
12 final String status;
13 final List<dynamic> tasks;
14 final void Function(String taskId, String newStatus) onTaskMoved;
15
16 @override
17 State<KanbanColumn> createState() => _KanbanColumnState();
18}
19
20class _KanbanColumnState extends State<KanbanColumn> {
21 bool _isDragOver = false;
22
23 @override
24 Widget build(BuildContext context) {
25 return DragTarget<String>(
26 onWillAcceptWithDetails: (details) => true,
27 onAcceptWithDetails: (details) {
28 setState(() => _isDragOver = false);
29 widget.onTaskMoved(details.data, widget.status);
30 },
31 onMove: (_) => setState(() => _isDragOver = true),
32 onLeave: (_) => setState(() => _isDragOver = false),
33 builder: (context, candidateData, rejectedData) {
34 return AnimatedContainer(
35 duration: const Duration(milliseconds: 150),
36 color: _isDragOver
37 ? Colors.blue.withOpacity(0.15)
38 : Colors.grey.withOpacity(0.05),
39 padding: const EdgeInsets.all(8),
40 child: Column(
41 children: [
42 Text(widget.status, style: const TextStyle(fontWeight: FontWeight.bold)),
43 ...widget.tasks.map((task) => Draggable<String>(
44 data: task['id'] as String,
45 feedback: Material(
46 elevation: 8,
47 child: Container(
48 padding: const EdgeInsets.all(12),
49 width: 200,
50 color: Colors.white,
51 child: Text(task['title'] ?? ''),
52 ),
53 ),
54 childWhenDragging: Opacity(
55 opacity: 0.4,
56 child: _TaskCard(task: task),
57 ),
58 child: _TaskCard(task: task),
59 )),
60 ],
61 ),
62 );
63 },
64 );
65 }
66}
67
68class _TaskCard extends StatelessWidget {
69 const _TaskCard({required this.task});
70 final dynamic task;
71
72 @override
73 Widget build(BuildContext context) {
74 return Card(
75 margin: const EdgeInsets.symmetric(vertical: 4),
76 child: Padding(
77 padding: const EdgeInsets.all(12),
78 child: Text(task['title'] ?? ''),
79 ),
80 );
81 }
82}

Expected result: Task cards can be dragged from one column to another. The destination column highlights on hover. Dropping the card calls onTaskMoved with the task ID and new status.

4

Wire the Custom Widget to FlutterFlow pages and Firestore

Add the KanbanBoard Custom Widget to your page. Create a Backend Query at the page level loading tasks from Firestore ordered by sortOrder. Pass the tasks list to the widget. In the widget's onTaskMoved parameter, connect a Custom Action that calls your Firestore update: update the task document's status field to the new column status. Use a Firestore Update Document action within the Action Flow — target the tasks collection, identify the document by the task ID passed from the widget, and update the status field.

Expected result: Moving a card between columns immediately updates the UI and writes the new status to Firestore. The page reflects the correct state after reload.

Complete working example

reorderable_list_widget.dart
1// FlutterFlow Custom Widget: SortableList
2// A full-featured ReorderableListView with Firestore integration
3// Parameters: tasks (JSON List), onReorderComplete (Action returning List)
4
5import 'package:flutter/material.dart';
6
7class SortableList extends StatefulWidget {
8 const SortableList({
9 super.key,
10 required this.tasks,
11 required this.onReorderComplete,
12 this.itemColor,
13 this.dragHandleColor,
14 });
15
16 final List<dynamic> tasks;
17 final Future<dynamic> Function(List<dynamic>) onReorderComplete;
18 final Color? itemColor;
19 final Color? dragHandleColor;
20
21 @override
22 State<SortableList> createState() => _SortableListState();
23}
24
25class _SortableListState extends State<SortableList> {
26 late List<dynamic> _items;
27
28 @override
29 void initState() {
30 super.initState();
31 _items = List<dynamic>.from(widget.tasks);
32 }
33
34 @override
35 void didUpdateWidget(SortableList oldWidget) {
36 super.didUpdateWidget(oldWidget);
37 // Sync if the parent data changes (e.g. after Firestore refresh)
38 if (oldWidget.tasks != widget.tasks) {
39 _items = List<dynamic>.from(widget.tasks);
40 }
41 }
42
43 void _onReorder(int oldIndex, int newIndex) {
44 setState(() {
45 if (newIndex > oldIndex) newIndex--;
46 final item = _items.removeAt(oldIndex);
47 _items.insert(newIndex, item);
48 });
49 // Write to Firestore only once — after the drag is complete
50 widget.onReorderComplete(_items);
51 }
52
53 @override
54 Widget build(BuildContext context) {
55 return ReorderableListView(
56 shrinkWrap: true,
57 onReorder: _onReorder,
58 proxyDecorator: (child, index, animation) {
59 return AnimatedBuilder(
60 animation: animation,
61 builder: (context, child) => Material(
62 elevation: 8 * animation.value,
63 borderRadius: BorderRadius.circular(8),
64 child: child,
65 ),
66 child: child,
67 );
68 },
69 children: [
70 for (final item in _items)
71 Container(
72 key: ValueKey(item['id']),
73 margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
74 decoration: BoxDecoration(
75 color: widget.itemColor ?? Colors.white,
76 borderRadius: BorderRadius.circular(8),
77 boxShadow: [
78 BoxShadow(
79 color: Colors.black.withOpacity(0.05),
80 blurRadius: 4,
81 offset: const Offset(0, 2),
82 )
83 ],
84 ),
85 child: ListTile(
86 title: Text(item['title'] ?? ''),
87 subtitle: item['subtitle'] != null
88 ? Text(item['subtitle'])
89 : null,
90 trailing: Icon(
91 Icons.drag_handle,
92 color: widget.dragHandleColor ?? Colors.grey,
93 ),
94 ),
95 ),
96 ],
97 );
98 }
99}

Common mistakes when creating a Drag-and-Drop Interface in FlutterFlow

Why it's a problem: Updating Firestore on every pixel of drag movement

How to avoid: Only write to Firestore inside onReorder (ReorderableListView) or onAcceptWithDetails (DragTarget) — these fire exactly once when the drag ends and the item is placed.

Why it's a problem: Using index-based keys (ValueKey(index)) instead of ID-based keys

How to avoid: Always use the document's unique Firestore ID as the key: ValueKey(item['id']). This remains stable regardless of position.

Why it's a problem: Not wrapping Draggable feedback in a Material widget

How to avoid: Wrap the feedback widget in Material with an elevation value (4-8) to give it a raised, floating appearance during drag.

Best practices

  • Use ReorderableListView for single-list sorting, Draggable+DragTarget for cross-container movement
  • Always batch Firestore order updates — one batch.commit() for all changed documents
  • Provide visual drag feedback: elevation on the dragged item, highlight color on valid drop targets
  • Show the original item at 40% opacity (childWhenDragging) so users maintain spatial context
  • Update local state immediately for optimistic UI, then sync Firestore in the background
  • Limit draggable lists to 200 items maximum — beyond that, virtual scrolling is needed for performance
  • Test drag interactions with both finger and stylus on tablet-sized screens

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a FlutterFlow app with a Kanban board. Write a Flutter Custom Widget using Draggable and DragTarget that shows three columns (Todo, In Progress, Done). Task cards should be draggable between columns. When a card is dropped, call a callback with the task ID and the new column status. Include visual feedback: highlight the drop target column and show an elevated ghost card under the finger during drag.

FlutterFlow Prompt

In FlutterFlow, I have a Custom Widget (SortableList) that calls onReorderComplete with the reordered list when a drag ends. How do I wire this callback in FlutterFlow's widget parameter settings to trigger a Custom Action that loops through the list and updates the sortOrder field on each Firestore task document?

Frequently asked questions

Does FlutterFlow have a built-in drag-and-drop widget?

No. FlutterFlow's visual editor does not expose Flutter's ReorderableListView, Draggable, or DragTarget widgets directly. You must implement drag-and-drop through Custom Widgets, which require the Pro plan ($70/mo).

Can I use ReorderableListView and DragTarget together in the same app?

Yes. Use ReorderableListView for sorting within a column and DragTarget for moving items between columns. You can even nest them — a Kanban board where each column is a DragTarget and each column's internal list is a ReorderableListView.

How do I persist the sort order when the app restarts?

Store a sortOrder integer field on each Firestore document. Update it with a batch write when the user finishes reordering. Load documents with .orderBy('sortOrder', 'asc') in your Backend Query. The list will reload in the correct order on every app start.

Will drag-and-drop work on Flutter web?

Yes. Flutter web supports mouse drag using the same Draggable and DragTarget APIs. On web, dragging uses mouse pointer events instead of touch events — the same code works across both platforms without changes.

How do I prevent some items from being draggable while others are?

In ReorderableListView, you cannot selectively disable reordering on specific items without a custom header widget pattern. In Draggable, wrap the non-draggable items in a plain widget without the Draggable wrapper — or set the Draggable's child to ignore pointer events when dragging should be disabled.

Can I add drag-and-drop without exporting the project?

Yes, for basic cases. Add the Custom Widget code directly in FlutterFlow's Custom Code section — you do not need to export the project. FlutterFlow compiles and runs Custom Widgets in Run Mode. You only need to export if you require packages not available via FlutterFlow's pubspec dependencies settings.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.