In Flutter, how do Draggable and DragTarget move a sticker from a tray onto a canvas, and what do feedback and onAcceptWithDetails do?
answer
- carrier and receiver matched by type
- feedback rendered in the Overlay
- childWhenDragging stays behind
- WithDetails replaced onAccept (3.14)
- details.offset is global
basics
~20 sWrap each tray sticker in a Draggable<T> carrying data and a feedback widget that follows the finger in the Overlay; the canvas is a DragTarget<T> whose onWillAcceptWithDetails filters payloads and whose onAcceptWithDetails receives the data and the global drop offset.
solid answer
~30 sEach tray item is a `Draggable<String>` (or `LongPressDraggable` inside a scrolling tray) with `data` set to the sticker's id, a `feedback` widget that follows the finger in the `Overlay`, and an optional `childWhenDragging` left in the tray. The canvas is a `DragTarget<String>`: its `builder` receives `candidateData` so it can highlight while a sticker hovers, `onWillAcceptWithDetails` decides whether to accept, and `onAcceptWithDetails` gets `DragTargetDetails` with the data and a global `offset`, which I convert with `globalToLocal` before placing the sticker. `onWillAccept` and `onAccept` are deprecated since 3.14. The feedback is outside the page's subtree, so I wrap it in `Material`.
code
dart · 60 linesimport 'package:flutter/material.dart';
class TrayItem extends StatelessWidget {
const TrayItem({super.key, required this.asset});
final String asset;
@override
Widget build(BuildContext context) {
return LongPressDraggable<String>(
data: asset,
// Feedback lives in the Overlay; Material gives it text and ink context.
feedback: Material(
type: MaterialType.transparency,
child: Image.asset(asset, width: 96),
),
childWhenDragging: Opacity(opacity: 0.3, child: Image.asset(asset, width: 64)),
child: Image.asset(asset, width: 64),
);
}
}
class StickerDropZone extends StatefulWidget {
const StickerDropZone({super.key});
@override
State<StickerDropZone> createState() => _StickerDropZoneState();
}
class _StickerDropZoneState extends State<StickerDropZone> {
final List<(String, Offset)> _placed = [];
@override
Widget build(BuildContext context) {
final ColorScheme scheme = Theme.of(context).colorScheme;
return DragTarget<String>(
onWillAcceptWithDetails: (details) => details.data.endsWith('.png'),
onAcceptWithDetails: (details) {
final box = context.findRenderObject()! as RenderBox;
// details.offset is global: the feedback's top-left at drop time.
final Offset local = box.globalToLocal(details.offset);
setState(() => _placed.add((details.data, local)));
},
builder: (context, candidateData, rejectedData) => DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: candidateData.isNotEmpty ? scheme.primary : scheme.outlineVariant,
width: 2,
),
),
child: Stack(
children: [
for (final (asset, at) in _placed)
Positioned(left: at.dx, top: at.dy, child: Image.asset(asset, width: 96)),
],
),
),
);
}
}go deeper
Know the pair: Draggable carries data and shows feedback, DragTarget receives it through onAcceptWithDetails, and the generic types must match.
Explain feedback in the Overlay, childWhenDragging, the will-accept and accept callbacks, and converting the global offset with globalToLocal.
Handle scrollable trays with LongPressDraggable, multi-touch with maxSimultaneousDrags, cancelled drops, and migrate deprecated onAccept code.
Decide when built-in Draggable is enough and when an editor needs its own drag model, for example for undo, collaboration or cross-window drops.
## The model: a carrier and a receiver Flutter's drag and drop is two widgets that never reference each other directly. - **`Draggable<T>`** wraps the thing the user picks up and carries a piece of `data` of type `T`. - **`DragTarget<T>`** is a region that can receive data of type `T`. While a drag is in progress, Flutter shows the draggable's `feedback` widget in an `Overlay`, following the pointer, and hit-tests under it to find targets. Matching is by type: a `DragTarget<String>` only considers draggables whose `data` is a `String` (or `null`). ## `Draggable` parameters that matter | Parameter | Role | |---|---| | `child` (required) | what sits in the tray when idle | | `feedback` (required) | what follows the finger, rendered in the `Overlay` | | `data` | the payload delivered to the target | | `childWhenDragging` | what stays in the tray during the drag; `null` keeps showing `child` | | `axis` | locks movement to horizontal or vertical | | `dragAnchorStrategy` | `childDragAnchorStrategy` (default) keeps the grab point; `pointerDragAnchorStrategy` puts the feedback's corner at the pointer | | `maxSimultaneousDrags` | limits multi-touch copies; 0 disables dragging | | `onDragStarted`, `onDragUpdate`, `onDragEnd` | lifecycle hooks | | `onDragCompleted` / `onDraggableCanceled` | accepted by a target / dropped elsewhere (with velocity and offset) | **`LongPressDraggable`** starts only after a long press (`delay` defaults to `kLongPressTimeout`, 500 ms) and gives haptic feedback on start. Use it inside a scrollable tray, so ordinary swipes still scroll. ## `DragTarget` callbacks 1. **`builder(context, candidateData, rejectedData)`** — rebuilt as drags enter and leave; `candidateData` holds the accepted payloads hovering now, so you can highlight the zone. 2. **`onWillAcceptWithDetails`** — return `true` to accept this payload when it enters. If neither will-accept callback is given, everything of the right type is accepted. 3. **`onAcceptWithDetails`** — called on drop with `DragTargetDetails`: `data`, and `offset`, the global position of the feedback's top-left (pointer position minus the drag anchor). Convert it with `globalToLocal` before placing. 4. **`onMove`** and **`onLeave`** — track hovering and exit. `onWillAccept` and `onAccept`, which received only the data, are **deprecated since Flutter 3.14** in favour of the `WithDetails` variants; they still compile in 3.47 but new code should not use them. ## Pitfalls interviewers look for - **Feedback lives in the Overlay.** It is inserted above the page, outside the widgets that surround the draggable, so it does not inherit their `Material` or local inherited widgets. Text inside a bare feedback shows the unstyled debug look; wrap it in `Material` (transparent is fine) and pass any data it needs directly. - **Null `data` is never delivered** through `onAcceptWithDetails`; the callback only runs when the draggable carries data. - **Wrong generic type** silently makes the target ignore the draggable — `DragTarget<Sticker>` will not react to a `Draggable<String>`. - **Offsets are global.** Placing a dropped sticker at `details.offset` inside a canvas that is not at the screen origin puts it in the wrong place. - **Multi-touch copies.** Without `maxSimultaneousDrags: 1`, two fingers can drag two copies of the same tray item. ## Draggable or a GestureDetector? - Use **`Draggable`/`DragTarget`** when something moves *between* regions and the receiver needs to know what arrived: tray to canvas, sticker to trash can, card to column. - Use a **`GestureDetector`** with pan or scale callbacks when an object moves *within* one region and you own its position, such as a sticker already on the canvas. There is no target to find, so the overlay and type matching add nothing. - Many editors combine both: the tray uses `LongPressDraggable`, and once the sticker lands it becomes a transformable widget driven by scale callbacks.
- Why use LongPressDraggable for items in a horizontally scrolling tray?A plain `Draggable` starts on any drag, so it competes with the tray's scrolling and users cannot swipe the tray. `LongPressDraggable` waits for a long press (500 ms by default) before picking the item up, so short swipes scroll and a deliberate hold starts the drag, with haptic feedback on start.
- How do you react when a sticker is dropped outside every target?Use the draggable's `onDraggableCanceled`, which receives the release velocity and offset, or `onDragEnd`, whose `DraggableDetails.wasAccepted` is false. Flutter removes the feedback without animating it back, so you decide whether to show a hint, animate a return yourself or ignore it.
saying these in an interview costs you the question
- The feedback widget inherits the Material and theme around the Draggable.
- details.offset is already in the drop target's local coordinates.
- A DragTarget<Object> and a DragTarget<String> behave the same for strings.
- onAccept is the current, recommended callback for drops.
- A Draggable with null data still triggers onAcceptWithDetails.