flutter_keyboard_controller 1.0.1 copy "flutter_keyboard_controller: ^1.0.1" to clipboard
flutter_keyboard_controller: ^1.0.1 copied to clipboard

Smooth, frame-by-frame keyboard animation tracking for Flutter. Includes KeyboardChatScrollView, KeyboardToolbar, KeyboardStickyView, KeyboardAwareScrollView, and KeyboardAvoidingView — everything you [...]

flutter_keyboard_controller #

Frame-by-frame keyboard tracking for Flutter — smooth animations, auto-scroll forms, chat UIs, sticky toolbars, and a full imperative API. Built on native CADisplayLink (iOS) and WindowInsetsAnimationCompat + OnApplyWindowInsetsListener (Android).

pub.dev License: MIT


Preview #

Form auto-scroll Chat lift Sticky toolbar
form-scroll chat-lift toolbar
KeyboardAwareScrollView auto-scrolls to the focused field including labels and error text KeyboardChatScrollView lifts the message list with 4 configurable behaviors KeyboardToolbar slides in sync with the keyboard, pixel-perfect

Why this library? #

Flutter's built-in keyboard handling #

Flutter ships two mechanisms:

Scaffold(resizeToAvoidBottomInset: true) (default)

  • Scaffold body shrinks in one jump when keyboard animation ends — not per-frame.
  • TextField.scrollPadding calls Scrollable.ensureVisible() on the raw TextField bounds — ignores labels or error text below the field.
  • Android: on some devices (Samsung) a keyboard-type switch causes a static layout pass with no animation, so Flutter's scroll simply doesn't fire.

MediaQuery.viewInsetsOf(context)

  • Returns height only after animation ends.
  • Every widget reading it rebuilds — cascades through the entire InheritedWidget tree.
  • No progress value (0→1), no lifecycle events, no interactive-dismiss tracking.

Deep comparison #

Flutter built-in flutter_keyboard_controller
Height timing After animation ends Every frame
Progress 0→1 progressNotifier
Lifecycle events willShow / didShow / willHide / didHide / move / interactive
iOS interactive dismiss Estimated CADisplayLink exact position
Android keyboard-type switch ❌ Missed on Samsung setOnApplyWindowInsetsListener safety net
Rebuild scope Entire MediaQuery subtree Single ValueListenableBuilder
Auto-scroll to field ✅ raw TextField only ✅ full widget bounds (label + error) via KeyboardScrollBoundary
Auto-scroll when resizeToAvoidBottomInset: false
Global dismiss (tap/drag) Per-ScrollView only KeyboardProvider.dismissBehavior
Chat lift behaviors KeyboardChatScrollView
Sticky widget above keyboard KeyboardStickyView
Prev/Next/Done toolbar KeyboardToolbar
dismiss(keepFocus, animated)
Preload keyboard (iOS lag) ❌ ~300 ms KeyboardController.preload()
Android soft-input mode Manifest only setInputMode() at runtime

Performance #

Flutter built-in: O(N) — rebuilds the entire MediaQuery subtree (Scaffold → theme → every consumer)
                         every time the keyboard opens or closes.

This library:     O(1) — only the specific Padding/Transform wrapper updates via ValueListenableBuilder.
                         Heavy widgets (lists, cards, images) remain completely static.

Always pass stable content as child: to ValueListenableBuilder so it never rebuilds per-frame:

// ✅ child: never rebuilds — only Padding wrapper does (~60×/s during anim)
ValueListenableBuilder<double>(
  valueListenable: context.keyboard.heightNotifier,
  child: const MyList(),
  builder: (_, height, child) => Padding(
    padding: EdgeInsets.only(bottom: height),
    child: child,
  ),
);

Which widget should I use? #

What are you building?
│
├─ Form with many text fields → KeyboardAwareScrollView
│
├─ Chat / AI-chat UI         → KeyboardChatScrollView + KeyboardStickyView
│
├─ Fixed layout (compose,    → KeyboardAvoidingView  (adjusts whole container)
│  search bar, login page)     OR KeyboardStickyView (just the input bar)
│
└─ Form + Prev/Next/Done     → KeyboardToolbar (pair with KeyboardAwareScrollView)

KeyboardAvoidingView vs KeyboardStickyView — most common confusion #

KeyboardAvoidingView KeyboardStickyView
What adjusts The entire layout container One specific widget
Content behind keyboard ✅ Never hidden ❌ Keyboard overlaps it
Use case Compose screen Chat input bar

Installation #

dependencies:
  flutter_keyboard_controller: ^1.0.0
  • Android: min SDK 24.
  • iOS: min iOS 13.

Quick start #

import 'package:flutter_keyboard_controller/flutter_keyboard_controller.dart';

void main() {
  runApp(
    // ⚠️ KeyboardProvider must wrap your entire app (above MaterialApp /
    // CupertinoApp). Placing it below Scaffold causes keyboard state to reset
    // on every route push/pop and bottom-sheet open.
    KeyboardProvider(
      dismissBehavior: KeyboardDismissBehavior.onTapAndDrag,
      child: MaterialApp(home: MyApp()),
    ),
  );
}

KeyboardProvider #

Root widget. Required for all features.

⚠️ Important: Always place KeyboardProvider above MaterialApp / CupertinoApp. If placed below Scaffold, keyboard state resets on every route push/pop and breaks overlay handling for dialogs and bottom sheets.

KeyboardProvider(
  enabled: true,
  dismissBehavior: KeyboardDismissBehavior.onTapAndDrag,
  child: MaterialApp(...),  // ✅ correct — wraps the entire app
)
KeyboardDismissBehavior Effect
manual Never auto-dismiss (default)
onTap Dismiss on tap outside
onDrag Dismiss on scroll
onTapAndDrag Both

Reading keyboard state #

final animation = context.keyboard;          // subscribes — widget rebuilds
final animation = context.keyboardOrNull;    // no subscription

KeyboardAwareScrollView #

When to use: Multi-field forms (sign-up, profile, settings).

How it works:

  • Adds bottom padding = keyboard height so maxScrollExtent grows → content is scrollable above keyboard.
  • _onFocusChanged fires on every focus change → addPostFrameCallback triggers _scrollToFocusedInput.
  • KeyboardGeometryService calculates the target scroll offset using the field's RenderBox coordinates.
  • _isDismissing + ModalRoute.isCurrent prevent unwanted scrolls when a bottom sheet opens.
  • Android: setOnApplyWindowInsetsListener catches static keyboard-type switches that WindowInsetsAnimationCompat misses.

vs Flutter built-in:

  • Flutter: body snaps, scrolls to raw TextField bounds only, fails on resizeToAvoidBottomInset: false.
  • This: smooth per-frame padding, scrolls to full widget height (labels + errors), works independently of Scaffold.
Scaffold(
  resizeToAvoidBottomInset: false,
  body: KeyboardAwareScrollView(
    padding: const EdgeInsets.all(16),
    children: [
      // Plain field — scrolled to raw TextField bounds
      TextField(decoration: InputDecoration(labelText: 'Email')),

      // Custom widget — KeyboardScrollBoundary tells the scroll view to
      // include the label AND the red error text below the field.
      // Without it, only the raw TextField bounds are used and the error
      // text gets hidden behind the keyboard.
      KeyboardScrollBoundary(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('Password'),
            const TextField(obscureText: true),
            const Text(
              'Must be at least 8 characters',
              style: TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ),
      ),

      FilledButton(onPressed: submit, child: const Text('Sign in')),
    ],
  ),
)

KeyboardScrollBoundary — for custom input widgets #

Wrap your custom widget's root once so KeyboardAwareScrollView measures the full widget height (label + input + error text). No per-screen config needed — every screen using KeyboardAwareScrollView benefits automatically.

// Inside AppTextInput.build() — wrap once, works everywhere:
return KeyboardScrollBoundary(
  child: Column(children: [label, textField, errorText]),
);

Parameters #

Param Default Description
children required Content widgets
padding null Outer padding
scrollPadding EdgeInsets.all(20) Gap between field and keyboard edge
animationDuration 300 ms Scroll animation duration
animationCurve Curves.easeOut Scroll animation curve
physics platform ScrollPhysics
scrollContextFinder null Override ancestor traversal

KeyboardAvoidingView #

When to use: Fixed layouts where the entire container must adapt to the keyboard — login screen, compose screen, search overlay.

How it works:

  • LayoutBuilder captures parent height once. ValueListenableBuilder on heightNotifier applies the behavior per-frame.
  • Always returns the same wrapper widget type regardless of keyboard state, so Flutter never unmounts the subtree and FocusNodes keep their state.

vs Flutter built-in:

  • Flutter's resizeToAvoidBottomInset: true snaps the body in one step.
  • KeyboardAvoidingView follows the keyboard smoothly every frame.
Scaffold(
  resizeToAvoidBottomInset: false,
  body: KeyboardAvoidingView(
    behavior: KeyboardAvoidingBehavior.padding,
    child: Column(children: [
      Expanded(child: content),
      inputBar,  // lifts smoothly with keyboard
    ]),
  ),
)

Behaviors #

behavior Effect Constraints
padding Adds paddingBottom Works with any parent
height Reduces maxHeight Parent must give loose constraints (Flexible, not Expanded)
position Translates view upward Content may go behind AppBar
translateWithPadding Half translate + half padding Middle ground

⚠️ height behavior requires a parent with loose constraints. Use Flexible instead of Expanded.

Why? Expanded passes tight constraints (exact size) to its child. When the Flutter layout engine receives tight constraints it ignores any attempt to shrink the child — KeyboardAvoidingView simply can't reduce its height. Flexible passes loose constraints (maximum size), which allows the view to actually shrink to dodge the keyboard.


KeyboardChatScrollView #

When to use: Chat, messaging, AI assistant — reversed list with a floating input bar.

How it works:

  • ListView(reverse: true) with padding.bottom adjusted per-frame via heightNotifier.
  • No viewport resize — keyboard overlays. Lifting is pure padding change (no jumpTo needed).
  • Four lift behaviors match popular apps.

vs Flutter built-in: No built-in equivalent. The naive resizeToAvoidBottomInset: true approach causes the list to flash/jump on keyboard open.

Scaffold(
  resizeToAvoidBottomInset: false,
  body: Stack(children: [
    Positioned.fill(
      child: KeyboardChatScrollView(
        liftBehavior: KeyboardLiftBehavior.whenAtEnd,
        extraBottomPadding: 72,  // floating input bar height
        safeAreaBottom: MediaQuery.of(context).viewPadding.bottom,
        children: messages.map((m) => MessageBubble(m)).toList(),
      ),
    ),
    ValueListenableBuilder<double>(
      valueListenable: context.keyboard.heightNotifier,
      child: InputBar(),
      builder: (_, kbH, child) => Positioned(
        left: 0, right: 0, bottom: kbH, child: child!,
      ),
    ),
  ]),
)

Lift behaviors #

liftBehavior When lifts Inspired by
always Every time keyboard opens Telegram
whenAtEnd Only when at newest message WhatsApp, ChatGPT
persistent Opens and stays lifted Claude.ai
never Never lifts Perplexity

KeyboardStickyView #

When to use: A single widget (input bar, send button) that must always sit just above the keyboard.

How it works:

  • Align(bottomCenter) + Padding(bottom: kbH + extraOffset) driven by heightNotifier.
  • Follows the keyboard pixel-perfectly every frame including iOS interactive swipe-dismiss.

vs Flutter built-in: MediaQuery.viewInsetsOf only updates after animation — widget teleports. KeyboardStickyView slides smoothly.

Stack(
  children: [
    Positioned.fill(child: chatList),
    KeyboardStickyView(
      offset: const KeyboardStickyOffset(closed: 0, opened: 8),
      child: InputBar(),
    ),
  ],
)

KeyboardToolbar #

When to use: Forms with multiple fields where Prev/Next/Done navigation improves UX.

How it works:

  • _ToolbarVisibility subscribes to lastEventNotifier (not heightNotifier) so it reacts to lifecycle events, not per-frame height.
  • Slides in/out via ClipRect + Align(heightFactor) in sync with keyboard animation.
  • KeyboardToolbarScaffold with resizeToAvoidBottomInset: false uses a Stack + Positioned.fill(VLB) so Positioned inside VLB issue is avoided.
  • Android: _isSwitching flag + _savedKbH keeps toolbar at correct height during keyboard-type switches.

vs Flutter built-in: No built-in toolbar. Workarounds using MediaQuery cause the toolbar to teleport (appear/disappear in one frame) instead of sliding.

KeyboardToolbarScaffold(
  resizeToAvoidBottomInset: false,
  toolbar: KeyboardToolbar(
    margin: const EdgeInsets.fromLTRB(8, 0, 8, 8),
    borderRadius: BorderRadius.circular(100),  // pill
    doneLabel: 'Xong',
    arrowColor: Colors.blue,
    doneColor: Colors.blue,
    onPrev: () => FocusScope.of(context).previousFocus(),
    onNext: () => FocusScope.of(context).nextFocus(),
    actions: [
      KeyboardToolbarAction(icon: Icons.tag, onPressed: insertTag),
      KeyboardToolbarAction(
        icon: Icons.photo_camera_outlined,
        onPressed: openCamera,
        isSelected: _cameraMode,
        selectedColor: Colors.blue,
      ),
    ],
  ),
  body: KeyboardAwareScrollView(children: [...]),
)

KeyboardToolbar parameters #

Param Default Description
doneLabel 'Done' Dismiss button text (multilang)
arrowColor primary Prev/Next arrow color
doneColor primary Done button color
showArrows true Show Prev/Next arrows
actions [] Custom icon buttons (≤ 5)
margin null Outer margin for floating pill effect
borderRadius null Corner radius for pill shape
toolbarScrollClearance 64 Extra scroll padding so fields clear the toolbar

KeyboardAnimation — raw values #

final animation = context.keyboard;

// Only this widget rebuilds per frame
ValueListenableBuilder<double>(
  valueListenable: animation.heightNotifier,
  child: const MyFAB(),
  builder: (_, height, fab) => Positioned(
    right: 16, bottom: 16 + height, child: fab!,
  ),
);
Notifier Type Description
heightNotifier ValueNotifier<double> Keyboard height in dp (0 when hidden)
progressNotifier ValueNotifier<double> Progress 0→1
isVisibleNotifier ValueNotifier<bool> Whether keyboard is visible
lastEventNotifier ValueNotifier<KeyboardEventData?> Last native event

Lifecycle events #

animation.lastEventNotifier.addListener(() {
  switch (animation.lastEvent?.type) {
    case KeyboardEventType.willShow: // about to appear
    case KeyboardEventType.didShow:  // fully visible
    case KeyboardEventType.willHide: // about to hide
    case KeyboardEventType.didHide:  // fully hidden
    case KeyboardEventType.move:     // per-frame during animation
    case KeyboardEventType.interactive: // iOS swipe-dismiss
  }
});

KeyboardController — imperative API #

await KeyboardController.dismiss();
await KeyboardController.dismiss(keepFocus: true);   // hide keys, cursor stays
await KeyboardController.dismiss(animated: false);   // iOS: instant

final visible = await KeyboardController.isVisible();
final state   = await KeyboardController.state();    // height, isVisible

KeyboardController.preload();                        // iOS: eliminate cold-start lag

// Android only
KeyboardController.setInputMode(AndroidSoftInputMode.adjustNothing);
KeyboardController.setDefaultMode();

Architecture notes #

Android — two-layer keyboard tracking #

This library uses two complementary mechanisms on Android:

  1. WindowInsetsAnimationCompat.Callback — tracks per-frame height during animated keyboard transitions. Fires willShow/move/didShow/willHide/didHide events.

  2. setOnApplyWindowInsetsListener (safety net) — catches static keyboard-type switches (e.g. text → number keyboard) where Android performs a layout pass with no animation. Without this, heightNotifier would stay at the old value and the toolbar/scroll would appear mispositioned.

The isAnimating flag prevents duplicate events when both fire for the same transition.

KeyboardGeometryService #

DOM traversal and scroll-offset math are extracted into KeyboardGeometryService (under lib/src/services/). This means:

  • KeyboardAwareScrollView is a thin UI layer — only renders and triggers events.
  • Geometry logic is independently unit-testable without rendering.

KeyboardScrollBoundary #

A zero-overhead marker widget (build returns child unchanged). KeyboardGeometryService.resolveTargetContext walks ancestors looking for it. Wrap your custom input's root widget once — no per-screen config.


API summary #

Widgets #

Widget Purpose
KeyboardProvider Root — required. Enables all features.
KeyboardScrollBoundary Marks full bounds of a custom input (label + input + error)
KeyboardAwareScrollView Form scroll — auto-scrolls focused field above keyboard
KeyboardAvoidingView Adjusts entire container with 4 behaviors
KeyboardChatScrollView Chat list with 4 lift behaviors
KeyboardStickyView Sticks one widget to keyboard top edge
KeyboardToolbar Prev/Next/Done toolbar above keyboard
KeyboardToolbarScaffold Convenience scaffold wiring KeyboardToolbar

Enums #

Enum Values
KeyboardDismissBehavior manual, onTap, onDrag, onTapAndDrag
KeyboardAvoidingBehavior padding, height, position, translateWithPadding
KeyboardLiftBehavior always, whenAtEnd, persistent, never
KeyboardEventType willShow, didShow, willHide, didHide, move, interactive
AndroidSoftInputMode adjustResize, adjustPan, adjustNothing, adjustUnspecified

Changelog #

See CHANGELOG.md.

License #

MIT © 2026 Tuan Nguyen Cong

1
likes
0
points
318
downloads

Publisher

unverified uploader

Weekly Downloads

Smooth, frame-by-frame keyboard animation tracking for Flutter. Includes KeyboardChatScrollView, KeyboardToolbar, KeyboardStickyView, KeyboardAwareScrollView, and KeyboardAvoidingView — everything you need to build polished keyboard-aware UIs on iOS and Android.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_keyboard_controller

Packages that implement flutter_keyboard_controller