skip to content

In Flutter, how do Draggable and DragTarget move a sticker from a tray onto a canvas, and what do feedback and onAcceptWithDetails do?

level: middleimportance: should knowfreq 36%

answer

  1. carrier and receiver matched by type
  2. feedback rendered in the Overlay
  3. childWhenDragging stays behind
  4. WithDetails replaced onAccept (3.14)
  5. details.offset is global

basics

~20 s

Wrap 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 s

Each 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 lines
dart
import '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

for a junior

Know the pair: Draggable carries data and shows feedback, DragTarget receives it through onAcceptWithDetails, and the generic types must match.

for a middle

Explain feedback in the Overlay, childWhenDragging, the will-accept and accept callbacks, and converting the global offset with globalToLocal.

for a senior

Handle scrollable trays with LongPressDraggable, multi-touch with maxSimultaneousDrags, cancelled drops, and migrate deprecated onAccept code.

for a principal

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.