Koji Wakamiya

Beyond Infinite Scroll: A Data-Driven Approach to Flutter Pagination

Why I built paging_view to bring the robustness of Android’s Paging 3 architecture to Flutter.

If you develop mobile apps, you inevitably face the requirement of implementing “infinite scroll.” By my estimation, it pops up about 1.5 times per app.

While it sounds like a common feature, implementing a robust infinite scroll is deceptively difficult. Relying solely on basic widgets often leads to implementations that feel “off” or fragile. Edge cases abound, and debugging scroll behaviors can be a time sink.

In this article, I’ll dive into the fundamentals of Paging and introduce paging_view.

If you’ve ever found yourself tangling UI code with data fetching logic just to get an infinite scroll working, you know the pain. paging_view is my answer to that chaos. Heavily inspired by the architecture of Android’s Jetpack Paging library, it prioritizes a strict separation of concerns, ensuring your UI remains focused on presentation while your data logic stays clean, testable, and robust.

The Reality of Paging APIs

When handling large or infinite datasets, loading everything at once is a non-starter. Especially on mobile, with strict constraints on network bandwidth and memory, fetching data in chunks (pages) of 10 to 20 items is the standard.

Defining the Key

Paging APIs generally rely on a “Key” to fetch the next chunk of data. There are a few common patterns:

  1. Offset/Page Number (Integer): Simple (Page 1, Page 2…), but fragile if items are added or removed in real-time (resulting in duplicate or missing items).
  2. Cursor/ID-based: Using the ID of the last item fetched. “Give me 20 items after ID xyz.” This is generally the most robust method for dynamic feeds like timelines, as it remains stable even if new items are inserted at the top.

paging_view is designed to be agnostic to your key type. Whether you use int for page numbers or String/DateTime for cursors, the architecture handles it seamlessly.

Auto-Fetch vs. Manual Fetch

There are two main UX patterns for loading the next page:

  1. Infinite Scroll: Automatically loads the next page when the user scrolls near the bottom. Seamless, but can use more data.
  2. “Load More” Button: User explicitly requests more data. Saves data, but interrupts the flow.

The choice depends on the specific requirements of your app. A chat app needs automatic loading; a search result footer might benefit from a manual trigger. A good paging library must support both without rewriting the core logic.

The Performance Challenge

Paging is fundamentally about managing resource constraints. Modern devices can handle thousands of items in memory, but there is always a limit.

Android’s Paging 3 library offers a sophisticated solution: it not only loads pages but also unloads them to keep memory usage constant, often integrating with a local database (like Room) via a RemoteMediator to act as a cache. This allows for truly “infinite” lists.

In Flutter, we rely on SliverList (or ListView) to recycle the Views (Widgets). While paging_view keeps the data objects in memory (unlike Paging 3’s sophisticated dropping mechanism), this is rarely a bottleneck for typical mobile use cases. If you are dealing with millions of records, the best practice in Flutter is to use a local database (like drift) as your source of truth and only load the IDs or lightweight objects into memory.

Why paging_view?

I created paging_view because I wanted the architectural cleanliness of Android Paging 3 in Flutter. Existing packages often mixed UI logic with data fetching logic, or didn’t support complex Sliver layouts natively.

paging_view | Flutter packageLike Android Jetpack's Paging library 3, manages data and displays paged lists.pub.dev

1. Separation of Concerns with DataSource

The core philosophy is the separation of the Data Layer from the UI Layer. You define your data fetching logic in a DataSource class, not inside your Widget.

class MyDataSource extends DataSource<int, MyEntity> {
  @override
  Future<LoadResult<int, MyEntity>> load(LoadAction<int> action) async {
    return switch (action) {
      Refresh() => await _fetch(0),
      Append(:final pageKey) => await _fetch(pageKey),
      Prepend() => const None(),
    };
  }
  // ... implementation details
}

The load method receives a sealed class LoadAction (Refresh, Append, or Prepend). This allows you to handle different loading directions safely and explicitly using Dart’s pattern matching.

2. Built for Slivers

paging_view is built on top of RenderSliver. This means SliverPagingList is a first-class citizen in a CustomScrollView. You can easily combine it with SliverAppBar, SliverGrid, or other slivers.

Under the hood, it uses a custom SliverBoundsDetector. Unlike the standard VisibilityDetector, this component detects when the scroll position enters the cacheExtent of the scroll view. This ensures that the “load next page” request fires before the user hits the bottom of the list, creating a smooth, uninterrupted scrolling experience.

You can check the full implementation here on GitHub, but the core logic resides in its performLayout. By utilizing SliverConstraints, it can detect exactly when it enters the viewport’s cache area:

// Core logic from RenderSliverBoundsDetector
@override
void performLayout() {
  // This sliver acts only as a marker and does not occupy any space.
  geometry = SliverGeometry.zero;

  // A sliver is considered "visible" if it is within the viewport's bounds
  // or the cache extent area.
  final hasReachedSliverStart = constraints.remainingCacheExtent > 0;
  final hasNotPassedSliver = constraints.scrollOffset <= 0;

  final isNowVisible = hasReachedSliverStart && hasNotPassedSliver;

  if (isNowVisible != _isVisible) {
    _isVisible = isNowVisible;
    // Notify the change via callback (typically in a post-frame callback)
    _notifyVisibilityChanged(_isVisible);
  }
}

This low-level control allows paging_view to behave natively within any scroll physics or layout.

3. Bidirectional Paging & The Center Anchor

One of the hardest features to implement in a custom scroll view is bidirectional paging (loading past items and future items), which is essential for Chat apps.

The difficulty lies in keeping the scroll position stable when items are added to the top (Prepend). Typically, adding items to the top pushes the content down, causing the user to lose their scroll position.

paging_view solves this with CenterPagingList, leveraging CustomScrollView’s center property.

  • Prepend items are rendered above the center anchor.
  • Append items are rendered below.
  • Center items act as the stable anchor.

By managing these three segments distinctively and merging them intelligently, CenterPagingList provides a stable bidirectional infinite scroll out of the box.

Conclusion

While there are many pagination packages out there, paging_view focuses on architectural stability and flexibility. It doesn’t force a specific UI on you; it gives you the tools to build complex, high-performance lists and grids using standard Flutter patterns.

If you are looking for a library that respects the DataSource pattern and plays perfectly with Slivers, give it a try.

paging_view | Flutter packageLike Android Jetpack's Paging library 3, manages data and displays paged lists.pub.dev koji-1009/paging_viewLike Android Jetpack's Paging library 3, manages data and displays paged lists.github.com

Demo: https://koji-1009.github.io/paging_view

← All posts