skip to content

Gestures & Focus

How touch, pointer and keyboard input reach widgets: gesture detectors, the arena that settles competing recognizers, and the focus tree that routes key events. Interviewers probe gesture conflicts.

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

explore

questions

15

In a Flutter mobile app, how do you dismiss the soft keyboard when the user taps outside a TextField or scrolls?

level: juniorimportance: must knowfreq 68%

answer

  1. keyboard follows text-field focus
  2. primaryFocus?.unfocus()
  3. onTapOutside default differs by platform
  4. keyboardDismissBehavior: onDrag
  5. hiding is not unfocusing

basics

~10 s

Move focus off the text field with FocusManager.instance.primaryFocus?.unfocus(), called from TextField.onTapOutside or a background tap, and set keyboardDismissBehavior to onDrag on scroll views. On touch Android and iOS, Flutter's defaults do neither.

solid answer

~40 s

The soft keyboard stays up while an editable text field holds focus, so dismissing it means moving focus away: `FocusManager.instance.primaryFocus?.unfocus()`, or `FocusScope.of(context).unfocus()`. Flutter's tap-outside default depends on the platform: a focused `TextField` sits in a `TextFieldTapRegion`, and its default action unfocuses on desktop, on the web and for mouse or stylus taps, but ignores touch taps in Android and iOS apps. So pass `onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus()`, or wrap the page in a `GestureDetector` whose `onTap` unfocuses. For lists, set `keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag`; the default is `manual`. `SystemChannels.textInput.invokeMethod('TextInput.hide')` hides the keyboard but leaves the field focused, so it is rarely the right call.

code

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

class OrderNotesPage extends StatelessWidget {
  const OrderNotesPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        // Close the keyboard as soon as the user drags the list.
        keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
        children: <Widget>[
          TextField(
            // Touch taps outside do not unfocus on Android and iOS by default.
            onTapOutside: (PointerDownEvent event) =>
                FocusManager.instance.primaryFocus?.unfocus(),
          ),
        ],
      ),
    );
  }
}

go deeper

for a junior

Remember that the keyboard follows focus: call FocusManager.instance.primaryFocus?.unfocus(), and set keyboardDismissBehavior to onDrag on lists.

for a middle

Explain why touch taps outside a TextField do not unfocus on Android and iOS by default, and where focus goes after unfocus().

for a senior

Choose between per-field onTapOutside and a screen-wide tap handler, and spot keyboard flicker caused by focus nodes recreated on rebuild.

for a principal

Decide an app-wide policy, such as a custom ScrollBehavior for dismissal, so every screen behaves the same without per-field code.

## Why the keyboard is tied to focus On Android and iOS the on-screen (soft) keyboard is an operating-system component. Flutter asks for it when an editable text widget — `TextField`, `TextFormField`, `CupertinoTextField`, all built on `EditableText` — gains **focus**, and releases it when that widget loses focus. So "dismiss the keyboard" in Flutter really means **move focus away from the text field**. Hiding the keyboard any other way leaves the field believing it is still being edited. Focus itself lives in the **focus tree**: one `FocusNode` holds the primary focus at a time (`FocusManager.instance.primaryFocus`), and `FocusScopeNode`s group nodes — every route pushed by a `Navigator` has its own scope, and `WidgetsApp` (behind `MaterialApp` and `CupertinoApp`) adds one at the top. ## The calls that remove focus | Call | What it does | Notes | |---|---|---| | `FocusManager.instance.primaryFocus?.unfocus()` | unfocuses whatever node has primary focus | works from any context, including callbacks with no `BuildContext` | | `FocusScope.of(context).unfocus()` | unfocuses the nearest enclosing scope of `context` | a no-op if that scope does not contain the focus | | `someNode.unfocus()` | unfocuses one specific node | only acts if that node or a descendant has focus | | `SystemChannels.textInput.invokeMethod('TextInput.hide')` | asks the platform to hide the keyboard | leaves the field focused | **There is always a primary focus.** `unfocus()` does not leave "nothing" focused; it hands focus to the nearest enclosing `FocusScopeNode` (the default `UnfocusDisposition.scope`), or to the scope's previously focused child with `UnfocusDisposition.previouslyFocusedChild`. Because the route's scope is not a text field, the keyboard closes. ## Tapping outside: the platform defaults `EditableText` wraps a focused field in a `TextFieldTapRegion`, which reports pointer-downs that land outside it. When you do not pass `onTapOutside`, the default action is platform-dependent: - **Windows, macOS, Linux:** the field unfocuses. - **Web (any platform):** the field unfocuses, touch included. - **Android and iOS apps:** mouse, stylus and unknown pointers unfocus, but **touch taps do not**. That last line is why "tap outside to close the keyboard" needs code on phones. Two common fixes: 1. Pass `onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus()` to each `TextField`. 2. Wrap the page body in a `GestureDetector` (or `Listener`) with `onTap` that unfocuses, so taps on empty areas close the keyboard for every field on the screen. The first is precise and keeps buttons working normally; the second is one widget for the whole screen, but taps that a descendant button wins never reach it. ## Scrolling: keyboardDismissBehavior Scroll views take `keyboardDismissBehavior`, an enum with two values: - `ScrollViewKeyboardDismissBehavior.manual` — the **default** from the base `ScrollBehavior`; scrolling leaves the keyboard alone. - `ScrollViewKeyboardDismissBehavior.onDrag` — the scroll view unfocuses as soon as a drag begins, closing the keyboard. `ListView`, `GridView`, `CustomScrollView` and `SingleChildScrollView` all accept it. If it is null, the value falls back to the scroll view's `scrollBehavior`, then to the ambient `ScrollConfiguration`, so an app-wide `ScrollBehavior` subclass can override `getKeyboardDismissBehavior` once. ## Pitfalls - **`TextInput.hide` alone.** The field keeps focus, so the caret stays, hardware key events still route to it, and focus-dependent UI still thinks editing is active. - **Unfocusing with the wrong context.** `FocusScope.of(context)` finds the scope *above that context*; from a widget outside the route that holds the field, it does nothing. `primaryFocus?.unfocus()` avoids the question. - **Losing traversal.** If there is no scope between the node and the root, focus falls back to `FocusManager.rootScope` and Tab traversal can stop working; apps built on `WidgetsApp` always have a scope, so this mostly bites custom setups. - **Rebuilding the field with a new node.** A `FocusNode` created in `build()` makes the keyboard flicker closed on rebuild, which looks like a dismissal bug but is a lifecycle bug.

  • Why is SystemChannels.textInput.invokeMethod('TextInput.hide') not a substitute for unfocus() in Flutter?
    It only asks the platform to hide the keyboard. The text field still holds primary focus, so it keeps its caret, hardware key events still go to it, and anything listening to its focus node still sees it as focused. Moving focus with `unfocus()` closes the keyboard as a consequence and leaves the focus tree consistent.
  • In Flutter, where does focus go after calling unfocus() on a focused TextField's node?
    With the default `UnfocusDisposition.scope`, focus moves to the nearest enclosing `FocusScopeNode`, usually the route's scope, and that scope's focus history is cleared, so the next Tab starts from the first node. `UnfocusDisposition.previouslyFocusedChild` instead returns focus to the scope's previously focused child. Some node always keeps primary focus.

saying these in an interview costs you the question

  • TextField closes the keyboard on outside taps on every platform by default
  • TextInput.hide is the proper way, since it also removes focus
  • ListView already dismisses the keyboard on drag by default
  • After unfocus() no node in the app has focus at all
  • FocusScope.of(context).unfocus() works from any context on the screen
open as a page

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%

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.

open as a page

In Flutter's gesture arena, how is a tap on an email tile resolved against the enclosing PageView's horizontal drag for the same pointer?

level: middleimportance: must knowfreq 46%

basics

~20 s

Both recognizers join the pointer's arena on pointer down. If the finger moves past the touch slop, the drag declares victory and the tap is rejected; if not, the arena is swept on pointer up and its first member, the innermost tap, wins.

open as a page

In Flutter, who should create and dispose a FocusNode, and what breaks when one is created inside build()?

level: middleimportance: must knowfreq 55%

basics

~20 s

A FocusNode is a long-lived object owned by a State: create it once and call dispose() in State.dispose. Creating one in build() swaps in a fresh node on every rebuild, leaking old nodes and dropping focus mid-typing.

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, what is the difference between AbsorbPointer and IgnorePointer, and which would you use to freeze an email compose form while sending?

level: juniorimportance: should knowfreq 44%

basics

~20 s

Both stop their children receiving pointers. IgnorePointer is invisible to hit testing, so widgets behind it still get touches; AbsorbPointer is hit itself and stops touches reaching anything behind. Use AbsorbPointer to freeze a compose form while sending.

open as a page

In a Flutter email list, why does adding onDoubleTap to a message tile make its onTap fire noticeably late?

level: middleimportance: should knowfreq 30%

basics

~20 s

DoubleTapGestureRecognizer holds the pointer's gesture arena after the first tap lifts, deferring the sweep that would award the single tap; only after kDoubleTapTimeout (300 ms) passes without a second tap does it release, and onTap fires.

open as a page

In Flutter, how do FocusTraversalGroup and OrderedTraversalPolicy fix a Tab order that the default policy gets wrong?

level: middleimportance: should knowfreq 32%

basics

~10 s

Tab order comes from the nearest FocusTraversalGroup's policy, by default ReadingOrderTraversalPolicy, which sorts by on-screen position. Wrap regions in their own FocusTraversalGroup, or use OrderedTraversalPolicy with FocusTraversalOrder(order: NumericFocusOrder(n)) for an explicit order.

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 email app, a swipe-to-archive tile inside a horizontal PageView blocks paging over it; how do you settle this with RawGestureDetector and a custom recognizer?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Both widgets add horizontal drag recognizers, and the tile's, being innermost, sees the move first and wins either direction. Subclass HorizontalDragGestureRecognizer to accept only leftward drags, and install it with RawGestureDetector and GestureRecognizerFactoryWithHandlers so rightward swipes reach the PageView.

open as a page

On a Flutter desktop point-of-sale screen, how would you bind F2, Ctrl+P and Escape with Shortcuts, Actions and Intents, and why might they not fire?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Shortcuts maps SingleActivator key combinations to Intent objects, and an Actions widget above the focused widget maps each Intent type to an Action. They fire only when primary focus is inside the Shortcuts subtree and an enabled Action is found.

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

In Flutter, when do you read keys with Focus.onKeyEvent versus a HardwareKeyboard.instance handler, and what replaced RawKeyEvent?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Focus.onKeyEvent receives KeyEvents only while focus is inside its subtree and returns a KeyEventResult that can stop bubbling. HardwareKeyboard.instance.addHandler sees every key app-wide. Both replace RawKeyEvent, RawKeyboard and FocusNode.onKey, deprecated in Flutter 3.19.

open as a page