flutter_keyboard_controller 1.0.4
flutter_keyboard_controller: ^1.0.4 copied to clipboard
Smooth, frame-by-frame keyboard animation tracking for Flutter. Includes KeyboardChatScrollView, KeyboardToolbar, KeyboardStickyView, KeyboardAwareScrollView, and KeyboardAvoidingView — everything you [...]
1.0.4 #
Android — Critical fix: black screen on cold-start from notification #
-
Root cause:
onAttachedToActivity()calledsetupKeyboardTracking()synchronously, which callsWindowCompat.setDecorFitsSystemWindows(window, false). This triggers a window re-layout that races with Flutter's SurfaceView/EGL-Vulkan initialisation. On cold-start from a system notification (e.g. a Live Activity / foreground-service notification), the Flutter engine attaches its surface at exactly the same time — the race caused the SurfaceView to render a blank black frame that never recovered. -
Fix — Native (
FlutterKeyboardControllerPlugin.kt):onAttachedToActivity()now only stores theActivityreference. All window configuration is deferred. -
Fix — Dart (
KeyboardProvider): On Android,initStateschedules apostFrameCallbackthat invokes the newsetupEdgeToEdgemethod channel. By the timepostFrameCallbackfires, Flutter has already committed its first frame and the SurfaceView is fully stable —setDecorFitsSystemWindows(false)and inset-listener registration happen safely with no race condition. -
isListenersAttachedguard: Added boolean flag tosetupInsetListenersto prevent duplicateViewCompat.setOnApplyWindowInsetsListener/WindowInsetsAnimationCompat.Callbackregistration on config-change reattach.
1.0.3 #
Performance — iOS: zero-allocation event emission #
- Before:
emit()created a new[String: Any]dictionary literal on every call — 60 heap allocations per second during keyboard animation (CADisplayLink ticks at 60 fps). - After: Pre-allocated
reusableEventdictionary is mutated in-place each call.FlutterEventSinkserializes synchronously on the main thread before the next tick, so there is no data-race risk. Result: 0 allocations per frame during animation.
Performance — Android: cached display density #
- Before:
decorView.resources.displayMetrics.densitywas read inonStart,onProgress(called 60× per second), andonEnd— three redundant property traversals per animation that always return the same value. - After:
densityis captured once inonPrepare(fires before the animation begins) and reused across all three callbacks. Display density never changes mid-animation.
Performance — Dart: KeyboardGeometryService context cache + type check #
-
Expando cache:
resolveTargetContextpreviously walked the element-tree ancestor chain on every scroll trigger for every focus change. Results are now cached perFocusNodein a weak-keyedExpando— subsequent calls for the same focused field return in O(1) without traversal. Cache entries are invalidated automatically when theFocusNodeis garbage-collected or its context is unmounted. -
is TextFieldinstead oftoString(): The plain-TextFieldancestor check waselement.widget.runtimeType.toString() == 'TextField', which allocates aStringobject per element during traversal. Replaced withelement.widget is TextField(direct vtable type check, zero allocation) after adding a non-circularpackage:flutter/material.dartimport forTextFieldonly.
1.0.2 #
Android — Critical fix: onStart always fired willShow regardless of direction #
-
Root cause:
bounds.upperBound.bottominWindowInsetsAnimationCompat.Callback.onStartis the maximum amplitude of the animation — it always equals the keyboard height regardless of whether the keyboard is opening or closing. The originalendHeight = bounds.upperBound.bottom; isShowing = endHeight > 0evaluated totrueunconditionally. As a resultkeyboardWillShowwas emitted even during dismiss, so_isDismissingwas never set totrueinKeyboardAwareScrollViewand the overscroll-clamp was always bypassed when pressing Done. -
Fix: Read the target inset via
ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottominonStart. Android completes a layout pass to the target state beforeonStartfires, so this value is always accurate —0for closing,> 0for opening or type-switch.Action targetImeBottomEvent emitted Keyboard opens > 0keyboardWillShow✓Keyboard closes (Done) 0keyboardWillHide✓Type switch (text → number) > 0keyboardWillShow✓
iOS — Fix: toolbar stretch/jitter during field switch (SafeArea → viewPaddingOf) #
-
Root cause:
SafeAreainside the toolbar row subscribes toMediaQuery.viewInsets. When the user switches focus between two text fields, Flutter's engine temporarily clears theTextInputclient, which causesMediaQuery.viewInsets.bottomto briefly reset to 0.SafeAreaimmediately adds ~34 px of home-indicator padding (since it thinks the keyboard is gone), stretching the toolbar for one frame. A frame later,viewInsetsis restored andSafeArearemoves the padding — the toolbar snaps back. -
Fix — Dart (
KeyboardToolbar): ReplacedSafeAreawith aValueListenableBuilderdriven byanimation.heightNotifier. Bottom padding is computed as(MediaQuery.viewPaddingOf(context).bottom − kbHeight).clamp(0, safeBottom).viewPaddingOfreturns the physical device safe-area constant (home-indicator height), which never changes with keyboard state — the toolbar height is now stable across all field switches.
KeyboardAwareScrollView — Synchronous delta translation (zero-jitter dismiss) #
-
Root cause: Bottom padding shrank per-frame via
_onHeightChanged, causingmaxScrollExtentto decrease each frame. Whenpixels > maxScrollExtent,BouncingScrollPhysicsproduced a "scroll up → spring back" glitch. The previous safety-clamp (jumpToindidHide) fired after the frame was already painted, adding a second visible snap. -
Fix — three coordinated changes:
-
Single source of truth:
_paddingNotifieris updated exclusively inside_onHeightChanged. Removed the direct assignments that were indidShowanddidHide, ensuringheightDeltais never accidentally zeroed before the clamp runs on the last frame. -
Synchronous delta translation:
jumpTo(expectedMaxExtent)runs before_paddingNotifier.value = newHeightsopos.maxScrollExtentstill reflects the old layout whenexpectedMaxExtent = (maxScrollExtent − heightDelta).clamp(0, ∞)is computed. BothpixelsandmaxScrollExtentchange by the same delta in the same frame — no spring lag, no 1-frame jitter. -
Clamp to
expectedMaxExtentnotpixels − Δ: Prevents over-scrolling whenpixelsis far above the new max.
-
KeyboardAwareScrollView — Kill switch for stale animateTo #
- Added
_scrollController.position.jumpTo(position.pixels)inwillHideanddidHidehandlers. CallsgoIdle()on the scroll position, immediately stopping any in-flightanimateTofrom_scrollToFocusedInputbefore the keyboard starts or finishes hiding.
KeyboardAwareScrollView — Duration-aware scroll trigger in didShow #
-
Root cause: On Android, static keyboard type switches (e.g. numpad → text) are handled by
setOnApplyWindowInsetsListenerwhich emitsdidShowwithduration = 0— nokeyboardMoveframes are produced, soheightNotifieris never updated._paddingNotifierremained at the old keyboard height,maxScrollExtentwas stale, andtargetOffsetwas clamped to the wrong value, leaving the focused field partially hidden. -
Fix: When
didShowcarriesduration == 0(instant switch),_paddingNotifier.valueis set explicitly to the new height before queuingaddPostFrameCallback— this forcesSingleChildScrollViewto rebuild with the correctmaxScrollExtentbefore_scrollToFocusedInputruns. Whenduration > 0(animated show),_paddingNotifierhas already been tracking per-frame via_onHeightChangedso_scrollToFocusedInputis called directly without delay.
KeyboardGeometryService — Plain TextField ancestor detection #
-
Root cause:
resolveTargetContextonly matchedFormFieldsubclasses (TextFormField). PlainTextFieldand multiline (maxLines > 1)TextFieldwidgets were not caught — the traversal fell back to the innerEditableTextbounds, which excludes the bottom border and padding. The focused field's bottom edge was measured too high, leaving the border hidden behind the keyboard. -
Fix: Added
element.widget.runtimeType.toString() == 'TextField'to the ancestor check (same string-comparison pattern used forKeyboardScrollBoundaryto avoid circular imports).FormFieldstill takes priority when found higher in the tree (e.g.TextFormField).
Example — iOS keyboard preload on startup #
- Added
KeyboardController.preload()call in_MyAppState.initStateviaaddPostFrameCallback. Warms up the iOS keyboard cache after the first frame so any screen's first keyboard appearance is instant with no cold-start delay.
1.0.1 #
Android — Critical fix: keyboard-type switch height tracking #
-
Root cause:
WindowInsetsAnimationCompat.Callbackonly fires during animated transitions. On Samsung and other Android devices, switching keyboard type (e.g. text → number) triggers a static layout pass with no animation —onProgress/onEndare never called, leavingheightNotifierstuck at the old keyboard height. The toolbar and auto-scroll would appear mispositioned after the switch. -
Fix: Added
setOnApplyWindowInsetsListeneras a safety-net listener alongside the animation callback. It fires on every inset change regardless of animation, ensuringheightNotifieris always up to date. TheisAnimatingflag prevents duplicate events when both listeners fire for the same transition.
Android — Toolbar positioning: Positioned inside ValueListenableBuilder inside Stack #
-
Root cause: Returning a
Positionedwidget from inside aValueListenableBuilder.builderthat is itself a child ofStackcauses undefined layout on Android —Positionedmust be a direct child ofStack's render tree to receiveStackParentData. -
Fix: Changed to
Positioned.fill(child: VLB(...))withAlign + Padding(bottom: kbH)inside the builder — the same pattern used byKeyboardStickyView. No more black screen or layout artifacts on Android.
Android — Toolbar type-switch flicker: pendingHide lambda capture #
-
Root cause: The deferred
didHidesuppression used aRunnable(captured by value).cancelPendingHide()setpendingHideRunnable = nullbut the already-queuedRunnablestill ran and emittedkeyboardDidHide, causingheightNotifierto reset and the toolbar to flicker. -
Fix: Changed
pendingHideRunnable: Runnable?to a Kotlin lambda variablependingHide: (() -> Unit)?. ThedecorView.post {}closure capturespendingHideby reference — aftercancelPendingHide()sets it tonull,pendingHide?.invoke()is safely skipped.
Architecture: KeyboardGeometryService #
- Extracted Element Tree traversal (
visitAncestorElements) and scroll-offset math (RenderBox.localToGlobal) from_KeyboardAwareScrollViewStateintolib/src/services/KeyboardGeometryService. KeyboardAwareScrollViewis now a thin UI layer; geometry logic is independently unit-testable without rendering a widget tree.
KeyboardAwareScrollView — replaced Future.delayed with addPostFrameCallback #
- Removed the arbitrary
focusScrollDelay(previously 120 ms) in favour ofWidgetsBinding.instance.addPostFrameCallback. _scrollGenerationcounter still cancels stale callbacks from rapid taps._isDismissing+ModalRoute.isCurrentguard still blocks scroll when a bottom sheet opens.- Android's new
setOnApplyWindowInsetsListenersets_isDismissingpromptly so the single-frame (≈ 16 ms) window is sufficient on all devices.
API cleanup (non-breaking) #
- Removed
focusScrollDelayparameter fromKeyboardAwareScrollView— was no longer used after theaddPostFrameCallbackmigration. Parameter was never mentioned in guides; removal has no user impact. - Removed private dead code
_resolveScrollContextLegacyfrom_KeyboardAwareScrollViewState.
KeyboardToolbar new features #
actions: List<KeyboardToolbarAction>— custom icon buttons with optional selected-state circle highlight.margin/borderRadius— floating pill style above keyboard.KeyboardDismissBehaviorinKeyboardToolbarScaffoldno longer requiresresizeToAvoidBottomInset: true; usesKeyboardStickyViewviaStack + Positioned.fillwhenfalse.toolbarScrollClearance— auto-injects extra scroll padding into nestedKeyboardAwareScrollViewviaKeyboardToolbarInsetInheritedWidget.
KeyboardChatScrollView — fix whenAtEnd mode missing rebuild #
_onScrollnow callssetStatewhen_wasAtEndchanges so_liftPaddingForre-evaluates correctly while keyboard is already visible.
README #
- Added visual preview table (GIFs).
- Added
KeyboardGeometryServiceand Android two-layer tracking architecture notes. - Replaced "2 large vs 18 micro rebuilds" with O(N) vs O(1) notation.
- Added
KeyboardScrollBoundaryusage in the mainKeyboardAwareScrollViewcode example. - Added
KeyboardProviderplacement warning (must wrapMaterialApp, notScaffold). - Added
heightbehaviorRenderBoxtight-vs-loose constraint explanation.
1.0.0 #
Breaking change #
KeyboardProvidernow acceptsdismissBehaviorparameter — defaults toKeyboardDismissBehavior.manual(no change in existing behavior).
New feature #
KeyboardDismissBehaviorenum added toKeyboardProvider:manual— keyboard never auto-dismissed (default, backward-compatible)onTap— dismiss when user taps anywhere outside a focused inputonDrag— dismiss when user starts scrollingonTapAndDrag— dismiss on both tap and scroll
0.0.4 #
- Fix: Remove Package.swift on iOS
0.0.3 #
Update README #
- Rewrote intro — removed inaccurate claim about
MediaQuery.viewInsetsOf, replaced with accurate description of targeted rebuild advantage - Updated comparison table:
MediaQuery.viewInsetsOfvs library, removed misleading rows, added real differentiators (rebuild scope, progress 0→1, event types, interactive dismiss,setInputMode,preload) - Updated installation version to
^0.0.2
0.0.2 #
Update README #
- Added full parameter tables for all widgets with defaults
- Added
KeyboardProviderdedicated section - Added
FocusedInputLayout,FocusedInputTextChangedEvent,FocusedInputSelectionChangedEventdocumentation - Added
KeyboardEventType.interactiveto event reference - Added
KeyboardControllerScopemethod table - Added
AndroidSoftInputModebehavior explanation - Added
KeyboardAwareScrollViewfull params + fallback note - Added
KeyboardAvoidingView.enabledparam + layout-stability explanation - Added
KeyboardChatScrollView.onEndVisible+safeAreaBottomdocumentation - Added
KeyboardAnimationevent listening example
0.0.1 #
Initial release #
- KeyboardProvider — root widget that tracks keyboard state via native platform channels. Wraps your app once; all descendants get access to live keyboard data.
- KeyboardAnimation — exposes
heightNotifier,progressNotifier, andisVisibleNotifierasValueNotifiers for efficient, targeted rebuilds. - KeyboardController — static API to dismiss keyboard, query visibility, and set Android soft-input mode.
- KeyboardChatScrollView — chat-optimised scroll view with four lift behaviours:
always,whenAtEnd,persistent,never. - KeyboardStickyView — sticks any widget to the top of the keyboard and animates it frame-by-frame.
- KeyboardToolbar — Prev / Next / Done toolbar above the keyboard with customisable labels, arrow colour, and Done colour.
- KeyboardToolbarScaffold — convenience scaffold that wires up
KeyboardToolbarwith a single line. - KeyboardAwareScrollView — auto-scrolls to keep the focused
TextFieldvisible as the keyboard opens. - KeyboardAvoidingView — lightweight alternative to Flutter's
resizeToAvoidBottomInsetfor fine-grained control. - iOS: frame-accurate keyboard tracking via
CADisplayLinkwith easing-curve interpolation. - Android: frame-accurate tracking via
WindowInsetsAnimationCompat. - Android:
setInputMode/setDefaultModesupport.