skip to content

Tap & Drag Detection

GestureDetector turns pointer events into taps, long presses, pans and scales, InkWell adds a ripple, and HitTestBehavior decides who gets touches on empty space. Interviewers probe that last one.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Flutter, when do you use GestureDetector versus InkWell for a tappable sticker, and what does InkWell add on top?

level: juniorimportance: must knowfreq 62%

answer

  1. widgets layer versus Material library
  2. ink painted on the Material ancestor
  3. Ink widget instead of a coloured Container
  4. focus and ActivateIntent come free
  5. InkResponse: unclipped, circular highlight

basics

~20 s

GestureDetector recognizes gestures and draws nothing; InkWell, from the Material library, wraps one and adds an ink splash painted on the nearest Material, focus with keyboard activation, hover and focus colours, and a disabled state when onTap is null.

solid answer

~40 s

`GestureDetector` is the widgets-layer tool: it turns pointer input into taps, long presses, drags, pans or scales and has no visual response, so I use it for the sticker being dragged and pinched on the canvas. For a tappable sticker thumbnail I use `InkWell`: it builds a `GestureDetector` internally and adds an ink splash painted on the nearest `Material` ancestor, `Focus` with `ActivateIntent` so Enter or Space presses it, hover and focus colours, and a disabled state when no tap callback is set. The splash disappears under any opaque child, so I paint backgrounds with `Ink` instead of a coloured `Container`. `InkWell` is `InkResponse` with a contained, rectangular splash.

code

dart · 30 lines
dart
import 'package:flutter/material.dart';

class StickerTile extends StatelessWidget {
  const StickerTile({super.key, required this.asset, required this.onPick});

  final String asset;
  final VoidCallback onPick;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;
    // Material is where the ink is painted; Ink paints the fill on that
    // Material, so the splash stays visible above it.
    return Material(
      type: MaterialType.transparency,
      child: InkWell(
        onTap: onPick, // null here would disable the ripple and focus
        borderRadius: BorderRadius.circular(12),
        child: Ink(
          decoration: BoxDecoration(
            color: scheme.surfaceContainerHighest,
            borderRadius: BorderRadius.circular(12),
          ),
          padding: const EdgeInsets.all(8),
          child: Image.asset(asset, width: 56, height: 56),
        ),
      ),
    );
  }
}

go deeper

for a junior

Know the split: GestureDetector for any gesture with no visuals, InkWell for Material-style pressable surfaces, and that InkWell needs a Material ancestor.

for a middle

Explain why the splash hides under an opaque Container, how Ink fixes it, and what InkWell adds for focus, hover and disabled states.

for a senior

Choose per surface with keyboard, desktop and web users in mind, and spot nested detectors where a button's recognizer swallows the outer tap.

for a principal

Set a team rule for pressable surfaces, for example Material buttons or InkWell by default, so focus and feedback stay consistent across the app.

## Two widgets, two jobs Both widgets turn touches into callbacks, but they sit at different layers of Flutter. - **`GestureDetector`** lives in the widgets layer. It creates gesture recognizers for whichever callbacks you pass — taps, double taps, long presses, vertical and horizontal drags, pans or scales — and draws nothing. It is the right tool when you need a gesture that is not a button press (dragging or pinching a sticker) or when the visual response is entirely yours. - **`InkWell`** lives in the Material library. It is a `StatefulWidget` that builds a `GestureDetector` internally and adds everything a Material-style pressable surface needs. ## What `InkWell` adds | Feature | `GestureDetector` | `InkWell` | |---|---|---| | Ink splash and highlight | no | yes, painted on the nearest `Material` | | Keyboard focus and activation | no | yes: `Focus` plus `ActivateIntent` and `ButtonActivateIntent` actions | | Hover and focus colours, mouse cursor | no | yes (`hoverColor`, `focusColor`, `overlayColor`, `mouseCursor`) | | Disabled state | not applicable | disabled when no tap-like callback is set | | Gestures supported | taps, long press, drags, pan, scale, force press | taps, double tap, long press, secondary taps | | Needs a `Material` ancestor | no | yes (`debugCheckHasMaterial`) | | Hit testing | `deferToChild` by default | the inner detector is `HitTestBehavior.opaque` | Both annotate the semantics tree with a tap action by default, so a screen reader can activate either; `GestureDetector` stops doing so only with `excludeFromSemantics: true`. ## The ink is drawn on the Material, not on the child This is the classic junior bug. The splash is painted on the `Material` widget above the `InkWell`. Anything opaque between them — a `Container` with a `color`, a `DecoratedBox`, an `Image` — covers the splash, so the tap works but nothing ripples. The fixes, straight from the framework docs: 1. Replace the `Container` or `DecoratedBox` with **`Ink`**, which paints its decoration or image onto the same `Material`, below the ink. 2. Or insert a second `Material` (often `MaterialType.transparency`) between the opaque widget and the `InkWell`. 3. Give `InkWell` the same `borderRadius` (or `customBorder`) as the shape so the splash does not spill outside rounded corners. In Material 3 the splash factory defaults to `InkSparkle` on Android (not web) and `InkRipple` elsewhere. ## `InkWell` versus `InkResponse` `InkWell` is a subclass of `InkResponse` that fixes two parameters: `containedInkWell: true` and `highlightShape: BoxShape.rectangle`, so the splash is clipped to the widget's rectangle. A bare `InkResponse` defaults to `containedInkWell: false` and a circular highlight that can overflow the box, which suits small icon-like targets; `IconButton` is built on it. ## Choosing in practice - A tappable sticker thumbnail in a tray, a list row, a card: **`InkWell`** (or a Material button), because users expect feedback and keyboard users expect focus. - The sticker on the editing canvas that is dragged, pinched and rotated: **`GestureDetector`**, because it needs scale callbacks and the visual response is the transform itself. - A custom-drawn control in a non-Material design: `GestureDetector`, adding your own focus handling with `Focus` or `FocusableActionDetector` if keyboard users must reach it. ## Pitfalls - Setting `onTap: null` on an `InkWell` "to disable it temporarily" also removes it from focus traversal and kills the ripple — which is usually what you want, but surprises people who expected a greyed ripple. - Wrapping an `ElevatedButton` in a `GestureDetector` to add a callback: the button's own recognizer wins for taps, so the outer `onTap` rarely fires. - Using `GestureDetector` for every button and then discovering there is no focus ring on desktop and web.

  • The tap handler fires but no ripple appears; what do you check first?
    Whether something opaque sits between the `Material` and the `InkWell` — a `Container` with a `color`, a `DecoratedBox` or an `Image`. The splash is painted on the `Material`, so it is hidden underneath. Swap the background for `Ink`, or add a transparent `Material` between the opaque widget and the `InkWell`. Also check there is a `Material` ancestor at all; debug builds assert on that.
  • Does GestureDetector expose anything to screen readers?
    Yes. By default it adds semantics actions for the callbacks it has, such as tap and long press, so assistive technology can trigger them. Passing `excludeFromSemantics: true` removes them. What it does not give you is keyboard focus or a visible focus state; `InkWell` adds those.

The Material is a sheet of paper and InkWell drips ink onto it; a coloured Container is a sticker glued on top of the paper, so the ink spreads underneath where nobody sees it. Ink paints the colour onto the paper itself.

saying these in an interview costs you the question

  • InkWell and GestureDetector are interchangeable apart from the ripple.
  • InkWell paints its splash on its own child, so any child works.
  • GestureDetector gives the widget keyboard focus automatically.
  • GestureDetector is invisible to screen readers unless you add Semantics.
  • InkResponse is just a deprecated name for InkWell.
open as a page

In Flutter, why does a GestureDetector around a padded sticker miss taps on its padding, and what do HitTestBehavior opaque, translucent and deferToChild change?

level: middleimportance: must knowfreq 55%

basics

~20 s

With a child, GestureDetector defaults to HitTestBehavior.deferToChild, so it is hit only where a descendant is hit and padding misses. opaque claims its whole box and blocks targets behind; translucent claims it but lets targets behind be hit too.

open as a page

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%

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.

open as a page

In Flutter, when would you use a Listener instead of a GestureDetector, and what does Listener deliberately not do for you?

level: middleimportance: should knowfreq 30%

basics

~20 s

Listener delivers raw pointer events (down, move, up, cancel, signal) and never joins the gesture arena, so it fires even when another recognizer wins; use it for drawing, pointer counting or observing input, and GestureDetector for anything that is actually a gesture.

open as a page

In a Flutter sticker editor, how do you let users drag, pinch-resize and rotate a sticker with GestureDetector, and why not combine onPan and onScale?

level: middleimportance: should knowfreq 42%

basics

~20 s

Use only the scale callbacks: onScaleUpdate reports focal point movement, a cumulative scale and a rotation, so one recognizer handles drag, pinch and rotate. Passing pan and scale together fails a debug assertion, because scale is a superset of pan.

open as a page

In a Flutter list of saved stickers, why does swiping a Dismissible assert that it is still part of the tree, and how do key, confirmDismiss and onDismissed fix it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

After its resize animation, Dismissible calls onDismissed and expects to disappear; if the item is not removed from the data in that same frame, the next build asserts. Give it a stable key from the item's id, and put any async confirmation in confirmDismiss.

open as a page