Build an interactive calendar using a Custom Widget that wraps table_calendar with LongPressDraggable and DragTarget widgets. Users long-press an event to drag it to a different date for rescheduling, with ghost previews showing the dragged item and target date highlights. Drop triggers a Firestore update to the event's date field. Add drag-to-resize for event duration by dragging the bottom edge to extend the endTime. All Firestore writes happen only on drop, not during drag movement, to avoid excessive writes.
Building a Drag-and-Drop Calendar in FlutterFlow
Read-only calendars are frustrating when users need to reschedule. This tutorial builds an interactive calendar where events can be dragged to new dates and resized by dragging their edges. The Custom Widget combines table_calendar with Flutter's drag-and-drop system for a native feel.
Prerequisites
- FlutterFlow project with Firestore events collection
- Events collection with date, title, and duration fields
- table_calendar package added to pubspec dependencies
- Basic understanding of FlutterFlow Custom Widgets
Step-by-step guide
Create the Firestore schema for calendar events
Create the Firestore schema for calendar events
In Firestore, create an events collection with fields: title (String), date (Timestamp), startTime (Timestamp), endTime (Timestamp), color (String, hex code), userId (String), description (String, optional). The date field stores the calendar day for query filtering. The startTime and endTime fields define the event's time range within that day. The color field lets users categorize events visually. Query events by userId and date range to load only the visible month's events.
Expected result: Firestore has an events collection with date, time range, and color fields for calendar display.
Build the Custom Widget with table_calendar and drag support
Build the Custom Widget with table_calendar and drag support
Create a Custom Widget called DraggableCalendar. Inside, use TableCalendar from the table_calendar package as the base. Override the calendarBuilders to make each day cell a DragTarget that accepts event data. For event markers under each date, build them as LongPressDraggable widgets so users can long-press to start dragging. The LongPressDraggable shows a ghost copy of the event chip while dragging. The DragTarget for each day cell highlights with a border color when an event is dragged over it. Pass events as a widget parameter (List of event maps) and an Action Parameter callback onEventMoved(eventId, newDate).
1// Custom Widget: DraggableCalendar (simplified core)2import 'package:flutter/material.dart';3import 'package:table_calendar/table_calendar.dart';45class DraggableCalendar extends StatefulWidget {6 final List<Map<String, dynamic>> events;7 final Function(String eventId, DateTime newDate) onEventMoved;8 final double width;9 final double height;1011 const DraggableCalendar({12 required this.events,13 required this.onEventMoved,14 required this.width,15 required this.height,16 });1718 @override19 State<DraggableCalendar> createState() => _DraggableCalendarState();20}2122class _DraggableCalendarState extends State<DraggableCalendar> {23 DateTime _focusedDay = DateTime.now();24 DateTime? _hoveredDay;2526 @override27 Widget build(BuildContext context) {28 return SizedBox(29 width: widget.width,30 height: widget.height,31 child: TableCalendar(32 firstDay: DateTime.utc(2024, 1, 1),33 lastDay: DateTime.utc(2030, 12, 31),34 focusedDay: _focusedDay,35 calendarBuilders: CalendarBuilders(36 defaultBuilder: (ctx, day, focused) {37 return _buildDayCell(day);38 },39 markerBuilder: (ctx, day, events) {40 return _buildEventMarkers(day);41 },42 ),43 ),44 );45 }4647 Widget _buildDayCell(DateTime day) {48 return DragTarget<Map<String, dynamic>>(49 onWillAcceptWithDetails: (details) {50 setState(() => _hoveredDay = day);51 return true;52 },53 onLeave: (_) => setState(() => _hoveredDay = null),54 onAcceptWithDetails: (details) {55 widget.onEventMoved(details.data['id'], day);56 setState(() => _hoveredDay = null);57 },58 builder: (ctx, candidates, rejects) {59 return Container(60 decoration: BoxDecoration(61 border: _hoveredDay == day62 ? Border.all(color: Colors.blue, width: 2)63 : null,64 ),65 child: Center(child: Text('${day.day}')),66 );67 },68 );69 }7071 Widget _buildEventMarkers(DateTime day) {72 final dayEvents = widget.events.where(73 (e) => isSameDay(DateTime.parse(e['date']), day)74 ).toList();75 return Wrap(76 children: dayEvents.map((event) {77 return LongPressDraggable<Map<String, dynamic>>(78 data: event,79 feedback: Material(80 child: Container(81 padding: const EdgeInsets.all(4),82 color: Colors.blue.withOpacity(0.7),83 child: Text(event['title'],84 style: const TextStyle(color: Colors.white)),85 ),86 ),87 childWhenDragging: Opacity(88 opacity: 0.3,89 child: _eventChip(event),90 ),91 child: _eventChip(event),92 );93 }).toList(),94 );95 }9697 Widget _eventChip(Map<String, dynamic> event) {98 return Container(99 margin: const EdgeInsets.all(1),100 padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),101 decoration: BoxDecoration(102 color: Colors.blue,103 borderRadius: BorderRadius.circular(4),104 ),105 child: Text(event['title'] ?? '',106 style: const TextStyle(color: Colors.white, fontSize: 10)),107 );108 }109}Expected result: A calendar widget where events can be long-pressed and dragged to different dates.
Handle the drop event to update Firestore
Handle the drop event to update Firestore
In the parent page, add the DraggableCalendar Custom Widget and bind the onEventMoved callback to an Action Flow. When the callback fires with an eventId and newDate, update the Firestore events document: set the date field to the new date Timestamp, and adjust startTime and endTime to the same time-of-day on the new date. Show a SnackBar confirming the reschedule with an Undo option that reverts to the original date. This ensures the Firestore write only happens once on drop, not during the drag movement across intermediate dates.
Expected result: Dropping an event on a new date updates the Firestore document and shows a confirmation with Undo.
Add visual feedback with ghost preview and target highlighting
Add visual feedback with ghost preview and target highlighting
In the Custom Widget, the LongPressDraggable feedback parameter shows a semi-transparent copy of the event chip that follows the user's finger. The childWhenDragging parameter reduces the original chip to 30% opacity so users see where the event came from. The DragTarget builder checks if a candidate is hovering and adds a blue border highlight to the day cell. When the drag enters a day, the border appears. When it leaves, the border disappears. This combination of ghost, dimmed source, and highlighted target gives users clear visual feedback throughout the drag operation.
Expected result: Dragging shows a ghost event chip, dims the source position, and highlights the target date cell.
Implement drag-to-resize for changing event duration
Implement drag-to-resize for changing event duration
For events displayed in a day detail view (a vertical timeline showing events as positioned Containers), add a GestureDetector on the bottom edge of each event Container. When the user drags this handle downward, increase the event's endTime in proportion to the vertical distance dragged. Use a Page State variable resizingEndTime to track the new end time during the drag and update the Container height in real-time. On drag end (onPanEnd), write the final endTime to Firestore. This allows users to extend or shorten events by dragging their bottom edge up or down.
Expected result: Users drag the bottom edge of an event to change its duration, with the change saved on release.
Complete working example
1FIRESTORE SCHEMA:2 events (collection):3 title: String4 date: Timestamp (calendar day)5 startTime: Timestamp6 endTime: Timestamp7 color: String (hex code)8 userId: String9 description: String (optional)1011CUSTOM WIDGET: DraggableCalendar12 Parameters:13 events: List<Map> (event data)14 onEventMoved: Action Parameter (eventId, newDate)15 16 TableCalendar:17 calendarBuilders:18 defaultBuilder → DragTarget per day cell19 onAcceptWithDetails → call onEventMoved callback20 Highlight border on hover21 markerBuilder → LongPressDraggable per event22 feedback: semi-transparent event chip (ghost)23 childWhenDragging: dimmed original chip (30% opacity)2425PAGE: CalendarPage26 Backend Query: events where userId == currentUser27 filtered by visible month date range28 DraggableCalendar Custom Widget29 events: query results30 onEventMoved: Action Flow31 → Update Firestore event document (date, startTime, endTime)32 → Show SnackBar with Undo option3334DAY DETAIL VIEW:35 Vertical timeline (positioned Containers per event)36 Each event Container:37 Height proportional to duration38 Bottom edge: GestureDetector for drag-to-resize39 onPanUpdate → update Page State resizingEndTime40 onPanEnd → write final endTime to Firestore4142VISUAL FEEDBACK:43 LongPressDraggable feedback → ghost chip follows finger44 childWhenDragging → source chip at 30% opacity45 DragTarget → blue border highlight on hover46 SnackBar → 'Event moved to {newDate}' with UndoCommon mistakes when developing a Custom Calendar View with Drag and Drop Events in
Why it's a problem: Writing to Firestore during drag movement across intermediate dates
How to avoid: Only write to Firestore on the drop event (onAcceptWithDetails), never during drag movement. The visual feedback is handled locally in widget state.
Why it's a problem: Not providing visual feedback during the drag operation
How to avoid: Use LongPressDraggable's feedback for a ghost chip, childWhenDragging for a dimmed source, and DragTarget's builder for border highlighting on hover.
Why it's a problem: Forgetting to adjust startTime and endTime when moving an event to a new date
How to avoid: When updating the date, also update startTime and endTime to the same hour and minute on the new date. Preserve the time-of-day while changing the calendar day.
Best practices
- Write to Firestore only on drop, never during drag movement
- Show ghost preview, dimmed source, and highlighted target for clear drag feedback
- Adjust all date-related fields (date, startTime, endTime) when rescheduling
- Add an Undo SnackBar after each move so users can revert accidental drops
- Load only the visible month's events to keep the widget responsive
- Use color-coded event chips for quick visual category identification
- Debounce duration resize updates if you need real-time preview during drag
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Build a FlutterFlow Custom Widget that wraps table_calendar with LongPressDraggable events and DragTarget day cells for drag-and-drop rescheduling. Include ghost preview during drag, target highlighting, and Firestore update on drop. Add drag-to-resize for event duration. Include the full Dart code for the Custom Widget.
Create a page with a Custom Widget calendar that shows events as colored chips on each date. Add a bottom sheet for event details when tapped, showing title, time range, and description with an edit button.
Frequently asked questions
Can I restrict which events are draggable?
Yes. Wrap the LongPressDraggable conditionally: only make events draggable if the current user is the event creator or if the event status allows editing. Non-draggable events render as static chips without drag behavior.
Does this work on web as well as mobile?
Yes. LongPressDraggable supports both touch (long-press on mobile) and mouse (click-and-hold on web). The interaction adapts to the input method automatically.
How do I handle recurring events?
Store a recurrence rule (weekly, monthly, custom) on the event document. When a recurring event is dragged, ask the user whether to move only this instance or all future occurrences. For single-instance changes, create an exception document.
Can I snap the drop to specific time slots?
Yes. In the day detail view, round the dropped position to the nearest 15 or 30-minute increment before updating the time fields. This prevents events from landing at odd times like 2:07 PM.
What happens if two users drag the same event simultaneously?
The last write wins in Firestore. To prevent conflicts, use a Firestore transaction that reads the current date before writing. If the date changed since the drag started, show a conflict message.
Can RapidDev help build advanced calendar features?
Yes. RapidDev can build interactive calendars with drag-and-drop, recurring events, multi-user scheduling, conflict detection, and integration with external calendar services like Google Calendar.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation