DEV Community

cyberail
cyberail

Posted on

Scroll Is Not Just Position: Introducing scroll_velocity_notifier

Available on pub.dev

When we work with scrolling in Flutter, we usually think in terms of position.

Where is the user right now?
How many pixels did they scroll?
Are we at the top or the bottom?

But scroll has another dimension that often gets ignored: speed.

How fast the user scrolls carries intent.
It tells you how something is happening, not just where.


Why scroll velocity matters

Think about these interactions:

  • An AppBar hides only when the user scrolls fast
  • A floating button fades out on quick flicks, but stays on slow scrolls
  • Animations trigger based on gesture intent, not offset
  • Overscroll feels elastic and alive instead of binary

All of these depend on scroll velocity, not scroll position.

Flutter exposes ScrollNotifications, but it doesn’t give you velocity in a form that is:

  • smooth
  • stable
  • reusable
  • decoupled from layout

That’s the gap this package tries to fill.


What scroll_velocity_notifier is

scroll_velocity_notifier is a small Flutter package that calculates real-time scroll velocity (in pixels per second) directly from ScrollNotifications.

It doesn’t replace scroll views.
It doesn’t change layouts.
It doesn’t force any architecture.

It simply observes scrolling and tells you:

“The user is scrolling this fast right now.”


Design goals

From the beginning, the goals were intentionally modest:

  • No layout impact
    The widget should not affect rendering or rebuild children.

  • No global state
    Everything should be opt-in and local.

  • No architectural opinion
    Works equally well with Bloc, Riverpod, streams, or plain callbacks.

  • Human-friendly values
    Raw velocity is noisy — the output should be usable directly.


How it works (conceptually)

The widget listens to ScrollUpdateNotifications and measures:

  • the change in scroll position (pixels)
  • the time between updates (microseconds)

From that, it calculates velocity in pixels per second.

To make the values stable enough for UI logic, it applies Exponential Moving Average (EMA) smoothing.
This removes jitter and produces velocity that feels intentional rather than mechanical.


Overscroll is optional — and intentional

In many Flutter apps, overscroll is not just an edge case — it’s part of the interaction.

When using physics like BouncingScrollPhysics, the scroll position can move beyond its bounds.
Sometimes you want velocity there. Sometimes you don’t.

For that reason, overscroll handling is explicit.

By default, velocity during overscroll is reported as 0.

You can opt into overscroll velocity when you need it:

ScrollVelocityProvider(
  includeOversScroll: true,
  onNotification: (notification, velocity) {
    debugPrint('Velocity: $velocity px/s');
    return false;
  },
  child: ListView(
    physics: const BouncingScrollPhysics(),
    children: const [],
  ),
);
Enter fullscreen mode Exit fullscreen mode

This keeps behavior predictable and avoids accidental effects.


Basic usage with a callback

The simplest way to use the package is via a callback:

ScrollVelocityProvider(
  onNotification: (notification, velocity) {
    debugPrint('Velocity: $velocity px/s');
    return false;
  },
  child: ListView.builder(
    itemCount: 50,
    itemBuilder: (context, index) {
      return ListTile(title: Text('Item $index'));
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

You wrap any scrollable widget and start receiving velocity values.

No controllers required.
No rebuilds triggered.
No extra layout widgets.


Using a StreamController (advanced usage)

For more complex scenarios, you can pass a StreamController to receive scroll velocity events.

This is useful when:

  • multiple consumers need the same scroll signal
  • velocity is handled outside the widget tree
  • you want to plug it into Bloc, Cubit, or analytics logic
final controller =
    StreamController<ScrollStreamNotification>.broadcast();

@override
void dispose() {
  controller.close();
  super.dispose();
}

ScrollVelocityProvider(
  controller: controller,
  child: ListView.builder(
    itemCount: 50,
    itemBuilder: (context, index) {
      return ListTile(title: Text('Item $index'));
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

Listening to the stream:

controller.stream.listen((event) {
  debugPrint('Velocity: ${event.velocity}');
});
Enter fullscreen mode Exit fullscreen mode

The widget never owns the controller.
If you pass one, you control its lifecycle.


Why a ProxyWidget?

Internally, the package is implemented using a ProxyWidget and ProxyElement.

This means:

  • children are not rebuilt
  • layout is untouched
  • the widget simply participates in the notification chain

It’s safe to use even in large or deeply nested scroll hierarchies.


What it is not

This package is intentionally small.

It does not:

  • animate anything for you
  • modify scroll physics
  • enforce UX decisions
  • hide behavior behind magic thresholds

It gives you data — you decide how to use it.


Final thoughts

Scrolling is one of the most frequent interactions in mobile apps, yet it’s often treated as a binary signal: moved or not moved.

Velocity adds nuance.

It captures intent, rhythm, and subtlety that position alone can’t express.

scroll_velocity_notifier is a small attempt to make that nuance accessible in Flutter — without adding complexity where it doesn’t belong.


📦 Package: scroll_velocity_notifier
🔗 Available on: pub.dev
🛠 Platform: Flutter / Dart

Top comments (0)