In Flutter, when would you use a Listener instead of a GestureDetector, and what does Listener deliberately not do for you?
answer
- raw pointer events, not gestures
- no gesture arena participation
- onPointerCancel can replace up
- no slop, no timeout, no tap
- hover belongs to MouseRegion
basics
~20 sListener 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.
solid answer
~40 s`Listener` sits one layer below gestures: it calls `onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerCancel` and `onPointerSignal` for every pointer event on its hit-test path, with position, pointer id and pressure. It does not take part in the gesture arena, so it keeps firing while a `GestureDetector` or scroll view recognizes the same touches. That makes it right for drawing strokes, counting fingers on a sticker canvas, or reading mouse-wheel signals, and wrong for taps: it has no touch slop, no long-press timing and no cancellation when the finger drifts, so an `onPointerUp` "tap" also fires at the end of a scroll. Hover without buttons belongs to `MouseRegion`.
code
dart · 40 linesimport 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Counts fingers on the sticker canvas without competing for gestures.
class PointerCounter extends StatefulWidget {
const PointerCounter({super.key, required this.child});
final Widget child;
@override
State<PointerCounter> createState() => _PointerCounterState();
}
class _PointerCounterState extends State<PointerCounter> {
final Set<int> _down = <int>{};
void _remove(PointerEvent e) => setState(() => _down.remove(e.pointer));
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (e) => setState(() => _down.add(e.pointer)),
onPointerUp: _remove,
onPointerCancel: _remove, // a pointer can end without an up event
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
debugPrint('wheel: ${event.scrollDelta}');
}
},
child: Stack(
children: [
widget.child,
if (_down.length >= 2)
const Positioned(top: 8, right: 8, child: Icon(Icons.pinch)),
],
),
);
}
}go deeper
Know that Listener gives raw pointer events while GestureDetector gives recognized gestures such as taps and drags.
Explain that Listener does not join the arena, what that means next to scroll views, and which callbacks it offers, including cancel and signal.
Use Listener deliberately for drawing, instrumentation or pointer counting, and avoid it for taps where slop, timing and cancellation matter.
Decide where raw input handling is justified in a codebase and keep it isolated, so gesture behaviour stays consistent and testable elsewhere.
## Two layers of input Flutter's input system has two layers: 1. **Pointer events** — `PointerDownEvent`, `PointerMoveEvent`, `PointerUpEvent`, `PointerCancelEvent` and friends. They describe what a finger, mouse or stylus is physically doing: position, pointer id, device kind, pressure. 2. **Gestures** — taps, long presses, drags, scales. Gesture recognizers watch pointer events and compete in the **gesture arena** to decide which gesture those events mean. `GestureDetector` works at layer 2. **`Listener`** works at layer 1: it calls you for every pointer event whose hit-test path includes it, without recognizing anything. ## What `Listener` offers | Callback | Fires on | |---|---| | `onPointerDown` / `onPointerUp` | contact starts and ends | | `onPointerMove` | movement while in contact (or buttons pressed) | | `onPointerCancel` | the system takes the pointer away, e.g. an incoming call | | `onPointerSignal` | discrete signals such as a mouse wheel `PointerScrollEvent` | | `onPointerPanZoomStart` / `Update` / `End` | trackpad pan and zoom gestures | Its `behavior` defaults to `HitTestBehavior.deferToChild`, like a `GestureDetector` with a child. ## The key difference: no arena A `Listener` **does not take part in the gesture arena**. Its callbacks fire whether or not some recognizer elsewhere wins. That is both its power and its danger: - **Power.** You can observe every finger on the sticker canvas while the canvas's own `GestureDetector`, or an enclosing scroll view, still recognizes gestures normally. You can count pointers, draw a stroke with pressure data, or drive a debugging overlay that shows touches. - **Danger.** Nothing stops a `Listener` from reacting to a touch that a scroll view also consumed. Wrap a list row in a `Listener` with `onPointerUp` as a "tap" and every scroll that ends on that row triggers it. There is no slop, no timeout and no cancellation when the finger moves away — all the things a `TapGestureRecognizer` adds. ## Choosing - **`GestureDetector`** for anything that is a gesture: taps, long presses, drags, pinches. It handles touch slop (`kTouchSlop`, 18 logical pixels), timing (`kLongPressTimeout`, 500 ms) and arena competition for you. - **`Listener`** when you need raw data or must not interfere: - drawing and handwriting, where every move event and its `pressure` matter; - counting simultaneous pointers or reacting the instant contact begins; - observing input under widgets that already handle gestures; - reading mouse-wheel `PointerSignalEvent`s. - **`RawGestureDetector`** when you need gesture recognition with custom recognizers or settings — that is arena territory. - **`MouseRegion`** for hover, enter and exit without buttons; `Listener` explicitly does not cover pure mouse hover. ## Pitfalls - Forgetting `onPointerCancel`: a pointer can disappear without an up event, leaving your "fingers down" set stale. - Treating `onPointerDown` as a tap: it fires even when the user then scrolls or drags. - Using `Listener` to "win" against a scroll view: it cannot win or lose anything; to change who wins, work with recognizers. - Assuming `onPointerMove` fires for a hovering mouse: movement without buttons is hover, which `MouseRegion` reports. ## A worked example On the sticker canvas, a translucent `Listener` wraps the whole editor and keeps a `Set<int>` of pointer ids: add on down, remove on up **and** on cancel. When the set holds two or more ids, the editor shows a pinch hint. The stickers below still use `GestureDetector` scale callbacks, and because the `Listener` never enters the arena, it neither steals their gestures nor loses events to them. The same pattern powers a freehand drawing layer: `onPointerMove` appends `event.localPosition` to the current stroke, and `event.pressure` sets its width where the device reports pressure.
- Why must a Listener-based pointer tracker handle onPointerCancel?The platform can take a pointer away mid-contact, for example when a system dialog appears, and then Flutter sends a `PointerCancelEvent` instead of a `PointerUpEvent`. A tracker that only removes pointers on up keeps a stale entry and believes a finger is still down.
saying these in an interview costs you the question
- Listener competes in the gesture arena like any recognizer.
- onPointerUp on a Listener is a safe replacement for onTap.
- Listener reports mouse hover without any button pressed.
- A Listener can stop a scroll view from scrolling.