dpad 3.1.1
dpad: ^3.1.1 copied to clipboard
Production-grade D-pad navigation for Flutter TV apps. Focus regions with memory, TV-correct directional traversal, focus effects and remote-key handling.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
3.1.1 - 2026-10-07 #
Fixed #
- Collapsed scrollables no longer swallow keys in a retry loop —
navigating away from a region whose matching-axis scrollable had a
zero along-axis viewport (an animated collapse keeping the subtree
mounted, or any layout glitch) consumed the key forever: the scroll
step was
viewportDimension * 0.8 == 0, the scroll animation completed instantly, and the retry re-entered the search every frame without ever moving. The move now bails out when a hop cannot make progress (the same< 0.5pxguardDpadScrollapplies to reveal scrolls), so the key falls through to the configured edge behavior. - App resume can no longer be downgraded by a queued restore — a focus-to-null event right before backgrounding (frames suspended, so the restore was still queued) dropped the resume flag: the pending restore then ran as a plain one, treated the unfocus as deliberate, and skipped restoration, leaving the app without focus after resume. The queued restore now keeps the strongest flag.
3.1.0 - 2026-10-07 #
Supports Flutter >= 3.19 / Dart >= 3.3 (was >= 3.24 / >= 3.5).
Added #
- Per-direction edge behaviors —
DpadRegiongainsleftEdge,rightEdge,upEdgeanddownEdge, overridinghorizontalEdge/verticalEdgefor a single boundary. Express layouts an axis-wide behavior cannot: a carousel that wraps to the right whileleftstill reaches the navigation rail, or a grid row that stops at its end without sealing off the sidebar. DpadEdgeBehavior.lineWrap— grid line wrap: right past the last cell of a row moves to the first cell of the row below, left past the first cell moves to the last cell of the row above (symmetrically for columns with up/down). Unlikewrap(same-row carousel), this is the typewriter behavior grids want: movement stays on the current line while it has cells ahead, then takes the line step — never a diagonal jump to another line. Ragged last rows are handled. At the grid's first/last line the key is consumed andonEdgefires; when the next line is not built yet (lazy grids), the engine scrolls and retries, like directional traversal.- RTL support — line wrap follows the reading order (in RTL layouts,
leftsteps to the line below andrightback up), and initial focus lands on the top-right item instead of the top-left one. Text-field caret exit rules and scroll-reveal already behaved correctly under RTL (Flutter's caret movement is logical, and scrollables report their directionality-adjusted axis).
Changed #
- Lowered the SDK floor to Flutter
>= 3.19/ Dart>= 3.3. The only code change:DpadElevationEffectpasses an explicitBoxShape.rectangletoAnimatedPhysicalModel, which Flutter 3.19 requires.
Fixed #
Dpad.enablednow truly freezes — previously only Dpad's own shortcuts stood down, but the framework's built-in arrow shortcuts still navigated with the very same TV policy, so focus kept moving. Arrows are now consumed without moving (while a text field is focused they fall through to plain caret editing, which still never moves focus), back/menu and app shortcuts stand down, in-flight wrap rewinds stop scrolling and grabbing focus, and programmaticDpadController.movecalls freeze too (returningfalse); select keys on the focused item keep working (the documented way for a frozen UI to switch navigation back on).- Programmatic focus outranks wrap rewinds —
Dpad.requestFocus,Dpad.clearFocusand Dpad's own focus recovery (list refresh, route pop, app resume) now invalidate any wrap-rewind chain still in flight, so a chain can no longer drag focus away from where the app deliberately put it. - Remapped movement keys no longer hijack typing — with a custom
DpadKeySet(WASD), pressing a remapped key while a text field was focused moved focus out and swallowed the character. Only the four physical arrow keys keep the caret-edge escape contract inside text fields; every other movement key falls through and types. - Multiline text fields escape vertically at the text bounds — previously a multiline field trapped focus forever: up/down never left it, stranding remote-only users. Up from the caret at the text's very start (first line) and down from the very end (last line) now move focus out, while inner-line arrows keep editing the caret.
- Hardened two crash paths found in review: a focus node detached from
the focus tree (element reuse after dispose) no longer falls into the
framework's
nearestScope!assert, and a wrap rewind that outlives its scrollable (page popped mid-hop) stops instead of touching a disposed position. - Lazy
lineWrapto unbuilt lines — a wrap move whose destination line is not built yet (long lazy grids, destination far off-screen) now scrolls the region's scrollable toward that line in bounded viewport hops and retries, instead of silently consuming the key at the first built line. The rewind resolves the extreme node geometrically, so wrap works no matter how far the destination sits from the viewport.
3.0.0 - 2026-06-13 #
A ground-up rewrite. 3.0 replaces the 2.x key interception and rule tables
with a TV-correct traversal engine built on Flutter's own focus primitives
(FocusTraversalPolicy, Shortcuts, Actions). The API is smaller,
declarative, and tuned for how TV interfaces actually behave.
Requires Flutter >= 3.24 / Dart >= 3.5.
Added #
DpadTraversalPolicy— directional navigation modeled on Android'sFocusFinder/Leanback: edge-based direction tests, beam preference (aligned candidates beat diagonal ones) and weighted distances.DpadRegion— declarative navigation areas:- region-first traversal: focus moves within the region before considering outside targets;
- per-region focus memory with
DpadEnterBehavior.restore(default),entryandnearest; - per-axis edge control with
DpadEdgeBehavior.leave,stopandwrap(carousel wrap-around); onEdgeandonFocusChangecallbacks; nesting supported.
- Lazy-list awareness — when no focus candidate exists but a scrollable
can still scroll in that direction (e.g. unbuilt
ListView.builderitems), the engine scrolls and retries instead of stopping. - Focus resilience — guaranteed startup focus (with
autofocustaking precedence), initial focus for routes pushed withoutautofocus, restoration when the focused widget is disposed, and restoration on app resume. The remote can never end up with nothing focused. DpadRegion.memoryKey— focus memory that persists across full subtree rebuilds (section/tab switchers), with position-aware restoration when the original items were recreated.- TV-correct text fields — arrows move the caret mid-text but navigate away at the caret's edge (and vertically in single-line fields), so a remote-only user is never trapped in a search box; IME composition owns all keys while active.
Dpad.debugOverlay— an on-screen focus inspector outlining the focused node with its label, region and geometry.Dpad.onFocusChange— a global hook on every focus move, for click sounds, haptics or analytics.DpadFocusableinteractions —onLongSelect(held center button) with select/long-select disambiguation, pressed-state visuals,onDirectionfor slider-style consumption of arrows, tap/click support for hybrid devices, andexcludeChildFocus(defaulttrue) so wrapped buttons never become double focus stops.- Class-based focus effects —
DpadScaleEffect(pressed push-down),DpadBorderEffect(layout-shift-free foreground border),DpadGlowEffect,DpadElevationEffect,DpadOpacityEffect,DpadTintEffect,DpadCustomEffect; composable viaeffects:lists. DpadTheme/DpadThemeData— app-wide defaults for effects, auto-scroll padding/duration/curve and long-select duration.DpadController(Dpad.of(context)) —move*(),next(),previous(),select(),back(),requestFocus(),clearFocus(),ensureVisible(),focused.DpadKeySet— semantic key mapping (up/down/left/right, select, back, menu) with TV-wide defaults andcopyWithremapping.- Text-input safety — directional keys, back/menu and app shortcuts
automatically stand down while an
EditableTextis focused. Dpad.wrap()— one-line installation throughMaterialApp.buildercovering every route, dialog and overlay.DpadScroll.ensureVisible— padded reveal that walks all scrollable ancestors, honors reversed axes and centers oversized items.
Changed #
- BREAKING
DpadNavigator→Dpad(recommended placement:MaterialApp(builder: Dpad.wrap())). - BREAKING
Dpadstatic helpers → instance methods onDpad.of(context)(navigateUp→moveUp,navigateNext→next,requestFocusSafely→requestFocus,scrollToFocus→ensureVisible). - BREAKING
DpadFocusable.buildersignature is now(context, DpadFocusState state, child);state.focusedreplaces theisFocusedboolean andstate.pressedis new.childis required. - BREAKING
onFocus/onBlurmerged intoonFocusChange(bool). - BREAKING selection is delivered through Flutter's
Actionssystem and raw key tracking instead ofconsumeKeyboardToken(); select fires once per press with correct repeat suppression. - BREAKING
onBackPressed→onBack(returnsbool:trueconsumes,falselets the framework handle it);onMenuPressed→onMenu;customShortcuts→shortcuts. - Auto-scroll defaults now come from the theme (
scrollPadding: 48).
Removed #
- BREAKING
FocusEffectsclosure factories — use theDpadEffectclasses. - BREAKING
FocusMemoryOptions,FocusHistoryManager,FocusHistoryEntry,onNavigateBackand the global focus-history stack — per-region memory (DpadRegion) replaces them. - BREAKING
RegionNavigationOptions,RegionNavigationRule,RegionNavigationStrategy,RegionNavigationManager,RegionAwareFocusTraversalPolicy,RegionTraversalGroup,RegionTraversalGroupScopeand the imperative node registration —DpadRegion+DpadTraversalPolicyreplace the entire rule system. - BREAKING
DpadFocusable.region/isEntryPoint/entryPriority— membership now comes from the widget tree (nearestDpadRegion); mark one item per region withentry: true.
Fixed #
- Wrapping a focusable Material button in
DpadFocusableno longer creates two d-pad stops. - Space/Enter and arrow keys are no longer hijacked while typing in a
TextField. - Holding the select key no longer fires
onSelectrepeatedly. - Auto-scroll now works correctly inside reversed scrollables and centers items larger than the viewport.
- Focus no longer dies when the focused item is removed from a refreshing list.
- The [Dpad] root keeps an identical widget structure for every
configuration, so flipping
enabledordebugOverlayat runtime never resets the application subtree.
2.0.2 - 2025-11-26 #
2.0.1 - 2025-11-25 #
2.0.0 - 2025-11-25 #
Added #
- Region-based navigation system (
RegionNavigationOptions,RegionNavigationRule, strategies,RegionAwareFocusTraversalPolicy).
1.2.2 - 2025-11-20 #
Added #
- Auto-scroll for focused widgets (
autoScroll,scrollPadding,Dpad.scrollToFocus).
1.1.0 - 2025-11-15 #
Added #
- Sequential navigation (
navigateNext/navigatePrevious), media key support, focus memory with history stack.
1.0.0 - 2025-11-10 #
- Initial release:
DpadNavigator,DpadFocusable,FocusEffects, programmaticDpadutilities.
