In Flutter, when do you use GestureDetector versus InkWell for a tappable sticker, and what does InkWell add on top?
answer
- widgets layer versus Material library
- ink painted on the Material ancestor
- Ink widget instead of a coloured Container
- focus and ActivateIntent come free
- InkResponse: unclipped, circular highlight
basics
~20 sGestureDetector 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 linesimport '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
Know the split: GestureDetector for any gesture with no visuals, InkWell for Material-style pressable surfaces, and that InkWell needs a Material ancestor.
Explain why the splash hides under an opaque Container, how Ink fixes it, and what InkWell adds for focus, hover and disabled states.
Choose per surface with keyboard, desktop and web users in mind, and spot nested detectors where a button's recognizer swallows the outer tap.
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.