skip to content

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%

answer

  1. raw pointer events, not gestures
  2. no gesture arena participation
  3. onPointerCancel can replace up
  4. no slop, no timeout, no tap
  5. hover belongs to MouseRegion

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.

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

for a junior

Know that Listener gives raw pointer events while GestureDetector gives recognized gestures such as taps and drags.

for a middle

Explain that Listener does not join the arena, what that means next to scroll views, and which callbacks it offers, including cancel and signal.

for a senior

Use Listener deliberately for drawing, instrumentation or pointer counting, and avoid it for taps where slop, timing and cancellation matter.

for a principal

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.