super_pagination 4.1.5
super_pagination: ^4.1.5 copied to clipboard
Powerful Flutter pagination library with built-in BLoC state management, 6+ view types (ListView, GridView, PageView, StaggeredGrid, ReorderableListView, Column, Row, Custom), advanced error handling [...]
Super Pagination #
v4 migration: the package name is now
super_pagination. Replace only the package segment in imports. ExistingSuperPagination*API names remain available as compatibility aliases, while new code can useSuperPagination*.
Production-ready Flutter pagination with built-in BLoC, search dropdowns, and error handling.
SuperPaginationListView.withProvider(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Type, SuperPaginationRequest>.future((req) => api.getProducts(req)),
itemBuilder: (context, items, index) => ProductTile(items[index]),
)
Features #
- 7 Widget Classes - ListView, GridView, PageView, StaggeredGrid, Column, Row, ReorderableList
- Super Search - Auto-positioning dropdown with key-based selection
- Built-in BLoC - State management included, or bring your own cubit
- Error Handling - 6 pre-built styles with first-page/load-more separation
- Stream Support - Future, Stream, and merged streams
- Data Operations - Insert, remove, update, replace, and refresh items with first/last/at targeting
- Auto Expiration - Configurable data age for global cubits
- Load-More Safety - Rapid scrolling can never trigger duplicate concurrent page requests; optional cross-page deduplication via
identityKey
GeniusLink Design System #
super_pagination now consumes super_core as its visual source of truth. Built-in loaders, empty states, error styles, search fields, overlays, focus states, radii, and spacing follow the active SuperMaterialThemeData automatically.
MaterialApp(
theme: SuperMaterialThemeData.light(
palette: SuperPalette.bluePalette,
),
darkTheme: SuperMaterialThemeData.dark(
palette: SuperPalette.bluePalette,
),
);
No package-specific extension is required for the default appearance. SuperSearchTheme.of(context) and SuperPaginationTheme.of(context) derive their values from the ambient SuperThemeData and ColorScheme. Register either extension only when you need a focused override:
final base = SuperSearchTheme.light();
SuperMaterialThemeData.light(
extensions: [
base.copyWith(searchBoxElevation: 0),
SuperPaginationTheme.light(),
],
);
The example application also uses the responsive SuperDeviceMode breakpoints and the shared SuperCard, SectionHeader, StatusPill, typography, spacing, radii, and semantic colors.
Load-More Safety Behaviour #
Spec 003 (specs/003-load-more-guard/) hardens load-more against duplicate concurrent fetches. The behaviour is state-guard only — there is no debounce or throttle timer.
- Exactly one load-more is active per cubit at any time. Additional
fetchPaginatedList()calls while a request is in flight are silently dropped at the cubit level. The widget layer additionally schedules the trigger viaSchedulerBinding.addPostFrameCallbackso multiple item builders firing in the same build pass collapse to a single callback. - Empty load-more responses end the list without appending. A short page (fewer items than
pageSize) IS appended and ends the list. A non-empty load-more page leaves the list open for further fetches. - Errors never end the list. A failed load-more sets
loadMoreErroron the loaded state;hasReachedEndis unchanged. A subsequentfetchPaginatedList()(orretryAfterError()undererrorRetryStrategy.manual) is allowed. - Refresh / search / filter changes reset every guard.
refreshPaginatedList()clears_isFetching, the in-flight per-page key,hasReachedEnd, and bumps the generation counter so any stale future or stream emission is discarded.
Optional: cross-page item deduplication #
Configure identityKey to drop items whose key already appears in an earlier accumulated page. The library never deduplicates silently — without identityKey, items are appended exactly as the provider returned them.
SuperPaginationCubit<Product, SuperPaginationRequest>(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider.future(api.fetchProducts),
identityKey: (product) => product.id,
);
Error retry behaviour #
After a load-more error, the errorRetryStrategy parameter controls the next attempt:
| Strategy | Effect |
|---|---|
none (default) |
Errors persist; only refreshPaginatedList() clears them |
manual |
retryAfterError() must be called explicitly |
automatic |
The next fetchPaginatedList() retries the failed page |
Scroll Anchor Preservation #
Spec 004 (specs/004-scroll-anchor-preservation/) preserves the user's
viewport position across load-more appends and prevents chain-triggered
auto-fetches caused by fast flings. Cross-references the load-more guard
above (spec 003) — the two features compose.
How it works #
- Capture (pre-fetch): just before each accepted load-more, the widget records a viewport anchor — the last fully-visible item's key (or its index, or a raw scroll offset, depending on what's available).
- Append: the cubit emits the new state with appended items as before; nothing is rendered out of order.
- Restore (post-frame): after the framework lays out the appended items, the package jumps the scroll back to the captured anchor so the on-screen content is visually stable.
- Suppression: the cubit ignores additional automatic load-more triggers until the user initiates a new drag-scroll gesture. This prevents a fast fling from chain-triggering page 3, 4, 5… in a single gesture.
Anchor strategy by view type #
| View type | Strategy | Backed by |
|---|---|---|
ListView |
key → itemIndex |
scrollview_observer ListObserver |
GridView |
key → itemIndex |
scrollview_observer GridObserver |
CustomScrollView/sliver |
key → itemIndex |
same observer (mounted on the items sliver) |
StaggeredGridView |
offset |
controller.position.pixels snapshot |
PageView |
no-op | out of scope (page-based, not scroll-based) |
ReorderableListView |
no-op | out of scope |
reverse: true (any view) |
no-op | out of scope |
Opt-out: preserveScrollAnchorOnAppend #
Every public wrapper (SuperPaginationListView, SuperPaginationGridView,
…) accepts preserveScrollAnchorOnAppend (default true). Setting it to
false reverts to pre-3.5.0 behaviour — anchor capture, restore, and the
post-append suppression flag are all disabled.
SuperPaginationListView<Product, SuperPaginationRequest>.withProvider(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider.future(api.fetchProducts),
itemBuilder: (context, items, index) => ProductTile(items[index]),
preserveScrollAnchorOnAppend: false, // legacy "stick to bottom" behavior
);
Troubleshooting #
- The viewport jumps a half-row. Anchor restore aligns the captured
item's trailing edge with the viewport bottom. Variable-height items
near the threshold may cause sub-row jumps; consider providing
itemKeyBuilderso the package uses the more precisekeystrategy. - Load-more never re-fires after restore. The post-append suppression
is cleared by the user's next drag-scroll. Programmatic
controller.jumpTo(...)does NOT clear it — callcubit.markUserScroll()explicitly if you want to bypass. - No anchor on PageView / ReorderableListView. These view types are out of scope by design — their scroll model is page-based or reorder-based, not append-based.
Installation #
dependencies:
super_pagination: ^4.1.3
import 'package:super_pagination/pagination.dart';
Quick Start #
ListView #
SuperPaginationListView.withProvider(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Type, SuperPaginationRequest>.future((req) => fetchProducts(req)),
itemBuilder: (context, items, index) => ListTile(
title: Text(items[index].name),
),
)
GridView #
SuperPaginationGridView.withProvider(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Type, SuperPaginationRequest>.future((req) => fetchProducts(req)),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context, items, index) => ProductCard(items[index]),
)
With External Cubit #
final cubit = SuperPaginationCubit<Product, SuperPaginationRequest>(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Type, SuperPaginationRequest>.future(fetchProducts),
dataAge: Duration(minutes: 5), // Auto-refresh stale data
);
SuperPaginationListView.withCubit(
cubit: cubit,
itemBuilder: (context, items, index) => ProductTile(items[index]),
)
Widget Classes #
| Widget | Layout | Use Case |
|--------|--------|----------|-|
| SuperPaginationListView | Vertical/horizontal list | Feeds, messages |
| SuperPaginationGridView | Multi-column grid | Catalogs, galleries |
| SuperPaginationColumn | Non-scrollable column | Embedded in ScrollView |
| SuperPaginationRow | Non-scrollable row | Chips, tags |
| SuperPaginationPageView | Swipeable pages | Onboarding, carousels |
| SuperPaginationStaggeredGridView | Masonry layout | Pinterest-style |
| SuperPaginationReorderableListView | Drag-and-drop | Task lists |
Each widget has two constructors:
.withProvider(...)- Creates cubit internally.withCubit(...)- Uses external cubit
Super Search #
Search components with auto-positioning overlay and key-based selection.
Basic Dropdown #
SuperSearchDropdown<Product, int>.withProvider(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Product, SuperPaginationRequest>.future((req) => api.search(req.searchQuery)),
searchRequestBuilder: (query) => SuperPaginationRequest(page: 1, pageSize: 20, searchQuery: query),
itemBuilder: (context, product) => ListTile(title: Text(product.name)),
keyExtractor: (product) => product.id,
onSelected: (product, id) => print('Selected: ${product.name} (ID: $id)'),
)
Key-Based Selection #
Select by ID instead of object reference - essential for edit forms and state management.
SuperSearchDropdown<Product, int>.withProvider(
// ... provider config
itemBuilder: (context, product) => ListTile(title: Text(product.name)),
// Key-based selection
keyExtractor: (product) => product.id,
selectedKey: selectedProductId,
onSelected: (product, id) => setState(() => selectedProductId = id),
selectedKeyLabelBuilder: (id) => 'Product #$id (loading...)',
showSelected: true,
)
Multi-Selection #
SuperSearchMultiDropdown<Product, int>.withProvider(
// ... provider config
keyExtractor: (product) => product.id,
selectedKeys: selectedIds,
onSelected: (products, ids) => setState(() => selectedIds = ids),
maxSelections: 5,
)
Bottom Sheet Mode #
For mobile-friendly selection, use displayMode: SearchDisplayMode.bottomSheet:
SuperSearchMultiDropdown<Product, int>.withProvider(
// ... provider config
displayMode: SearchDisplayMode.bottomSheet,
bottomSheetConfig: SuperSearchBottomSheetConfig(
title: 'Select Products',
confirmText: 'Done',
showSelectedCount: true,
showClearAllButton: true,
heightFactor: 0.85,
),
hintText: 'Tap to search...',
onSelected: (products, ids) => setState(() => selectedIds = ids),
)
| Display Mode | Description |
|--------------|-------------|-|
| SearchDisplayMode.overlay | Default dropdown overlay |
| SearchDisplayMode.bottomSheet | Fullscreen bottom sheet |
Components #
| Component | Description |
|-----------|-------------|-|
| SuperSearchDropdown<T, K> | Single-selection search dropdown |
| SuperSearchMultiDropdown<T, K> | Multi-selection with chips |
| SuperSearchController<T, K> | Controller for programmatic control |
| SuperSearchBox<T, K> | Standalone search input |
| SuperSearchOverlay<T, K> | Standalone results overlay |
| SuperSearchTheme | ThemeExtension for styling |
Configuration #
SuperSearchDropdown<Product, int>.withProvider(
// ...
searchConfig: SuperSearchConfig(
debounceDelay: Duration(milliseconds: 500),
minSearchLength: 2,
searchOnEmpty: false,
),
overlayConfig: SuperSearchOverlayConfig(
position: OverlayPosition.auto,
maxHeight: 400,
animationType: OverlayAnimationType.fadeScale,
),
)
SuperSearchDropdown Parameters #
Core Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| request | SuperPaginationRequest | Yes*| - | Pagination config (for .withProvider) |
| provider | SuperPaginationProvider<T, SuperPaginationRequest> | Yes* | - | Data source (for .withProvider) |
| cubit | SuperPaginationCubit<T, SuperPaginationRequest> | Yes* | - | External cubit (for .withCubit) |
| searchRequestBuilder | SuperPaginationRequest Function(String) | Yes | - | Builds request from search query |
| itemBuilder | Widget Function(BuildContext, T) | Yes | - | Builds each result item |
Selection Callback
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| onSelected | void Function(T, K)? | No | null | Called with item and key when selected |
| onChanged | ValueChanged<String>? | No | null | Called when text changes |
Key-Based Selection
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| keyExtractor | K Function(T)? | No | null | Extracts unique key from item |
| selectedKey | K? | No | null | Currently selected key |
| selectedKeyLabelBuilder | String Function(K)? | No | null | Label for pending key |
| selectedKeyBuilder | Widget Function(BuildContext, K, VoidCallback)? | No | null | Custom pending key widget |
Show Selected Mode
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| showSelected | bool | No | false | Show selected item instead of search box |
| initialSelectedValue | T? | No | null | Pre-selected item on load |
| selectedItemBuilder | Widget Function(BuildContext, T, VoidCallback)? | No | null | Custom selected item widget |
Search Box Appearance
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| decoration | InputDecoration? | No | null | TextField decoration |
| style | TextStyle? | No | null | Text style |
| prefixIcon | Widget? | No | null | Leading icon |
| suffixIcon | Widget? | No | null | Trailing icon |
| showClearButton | bool | No | true | Show clear button |
| borderRadius | BorderRadius? | No | null | Border radius |
Input Configuration
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| textInputAction | TextInputAction | No | search | Keyboard action button |
| textCapitalization | TextCapitalization | No | none | Text capitalization |
| keyboardType | TextInputType | No | text | Keyboard type |
| inputFormatters | List<TextInputFormatter>? | No | null | Input formatters |
| maxLength | int? | No | null | Max input length |
Validation
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| validator | String? Function(String?)? | No | null | Validation function |
| autovalidateMode | AutovalidateMode? | No | null | When to validate |
Overlay State Builders
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| loadingBuilder | WidgetBuilder? | No | null | Loading state widget |
| emptyBuilder | WidgetBuilder? | No | null | Empty results widget |
| errorBuilder | Widget Function(BuildContext, Exception)? | No | null | Error state widget |
| headerBuilder | WidgetBuilder? | No | null | Dropdown header |
| footerBuilder | WidgetBuilder? | No | null | Dropdown footer |
| separatorBuilder | IndexedWidgetBuilder? | No | null | Item separator |
| overlayDecoration | BoxDecoration? | No | null | Overlay container decoration |
Configuration Objects
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| searchConfig | SuperSearchConfig | No | SuperSearchConfig() | Search behavior config |
| overlayConfig | SuperSearchOverlayConfig | No | SuperSearchOverlayConfig() | Overlay appearance config |
Cubit Options (.withProvider only)
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| listBuilder | List<T> Function(List<T>)? | No | null | Transform items |
| onInsertionCallback | void Function(List<T>)? | No | null | Called on data load |
| maxPagesInMemory | int | No | 5 | Max cached pages |
| retryConfig | RetryConfig? | No | null | Retry configuration |
| dataAge | Duration? | No | null | Data expiration |
| orders | SortOrderCollection<T>? | No | null | Sort orders |
| logger | Logger? | No | null | Debug logger |
SuperSearchMultiDropdown Parameters #
Multi Core Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| request | SuperPaginationRequest | Yes*| - | Pagination config (for .withProvider) |
| provider | SuperPaginationProvider<T, SuperPaginationRequest> | Yes* | - | Data source (for .withProvider) |
| cubit | SuperPaginationCubit<T, SuperPaginationRequest> | Yes* | - | External cubit (for .withCubit) |
| searchRequestBuilder | SuperPaginationRequest Function(String) | Yes | - | Builds request from search query |
| itemBuilder | Widget Function(BuildContext, T) | Yes | - | Builds each result item |
Multi Selection Callback
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| onSelected | void Function(List<T>, List<K>)? | No | null | Called with items and keys when selection changes |
| onChanged | ValueChanged<String>? | No | null | Called when text changes |
| maxSelections | int? | No | null | Maximum items to select |
Multi Key-Based Selection
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| keyExtractor | K Function(T)? | No | null | Extracts unique key from item |
| selectedKeys | List<K>? | No | null | Currently selected keys |
| selectedKeyLabelBuilder | String Function(K)? | No | null | Label for pending keys |
| selectedKeyBuilder | Widget Function(BuildContext, K, VoidCallback)? | No | null | Custom pending key chip |
Multi Show Selected Mode
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| showSelected | bool | No | true | Show selected chips below search |
| initialSelectedValues | List<T>? | No | null | Pre-selected items on load |
| selectedItemBuilder | Widget Function(BuildContext, T, VoidCallback)? | No | null | Custom selected chip |
Multi Selected Items Layout
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| selectedItemsWrap | bool | No | true | Wrap chips or scroll horizontally |
| selectedItemsSpacing | double | No | 8.0 | Horizontal spacing between chips |
| selectedItemsRunSpacing | double | No | 8.0 | Vertical spacing when wrapped |
| selectedItemsPadding | EdgeInsets | No | EdgeInsets.only(top: 12) | Padding around chips container |
Multi Search Box Appearance
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| decoration | InputDecoration? | No | null | TextField decoration |
| style | TextStyle? | No | null | Text style |
| prefixIcon | Widget? | No | null | Leading icon |
| suffixIcon | Widget? | No | null | Trailing icon |
| showClearButton | bool | No | true | Show clear button |
| borderRadius | BorderRadius? | No | null | Border radius |
Multi Input Configuration
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| textInputAction | TextInputAction | No | search | Keyboard action button |
| textCapitalization | TextCapitalization | No | none | Text capitalization |
| keyboardType | TextInputType | No | text | Keyboard type |
| inputFormatters | List<TextInputFormatter>? | No | null | Input formatters |
| maxLength | int? | No | null | Max input length |
Multi Validation
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| validator | String? Function(String?)? | No | null | Validation function |
| autovalidateMode | AutovalidateMode? | No | null | When to validate |
Multi Overlay State Builders
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| loadingBuilder | WidgetBuilder? | No | null | Loading state widget |
| emptyBuilder | WidgetBuilder? | No | null | Empty results widget |
| errorBuilder | Widget Function(BuildContext, Exception)? | No | null | Error state widget |
| headerBuilder | WidgetBuilder? | No | null | Dropdown header |
| footerBuilder | WidgetBuilder? | No | null | Dropdown footer |
| separatorBuilder | IndexedWidgetBuilder? | No | null | Item separator |
| overlayDecoration | BoxDecoration? | No | null | Overlay container decoration |
Multi Configuration Objects
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| searchConfig | SuperSearchConfig | No | SuperSearchConfig() | Search behavior config |
| overlayConfig | SuperSearchOverlayConfig | No | SuperSearchOverlayConfig() | Overlay appearance config |
Multi Cubit Options (.withProvider only)
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|-|
| listBuilder | List<T> Function(List<T>)? | No | null | Transform items |
| onInsertionCallback | void Function(List<T>)? | No | null | Called on data load |
| maxPagesInMemory | int | No | 5 | Max cached pages |
| retryConfig | RetryConfig? | No | null | Retry configuration |
| dataAge | Duration? | No | null | Data expiration |
| orders | SortOrderCollection<T>? | No | null | Sort orders |
| logger | Logger? | No | null | Debug logger |
Error Handling #
Separate First-Page and Load-More Errors #
SuperPaginationListView.withProvider(
// ...
firstPageErrorBuilder: (context, error, retry) => CustomErrorBuilder.material(
context: context,
error: error,
onRetry: retry,
title: 'Failed to load',
),
loadMoreErrorBuilder: (context, error, retry) => CustomErrorBuilder.compact(
context: context,
error: error,
onRetry: retry,
),
)
Pre-Built Styles #
| Style | Best For |
|-------|----------|-|
| CustomErrorBuilder.material() | First page errors |
| CustomErrorBuilder.compact() | Load more errors |
| CustomErrorBuilder.card() | Card-based UIs |
| CustomErrorBuilder.minimal() | Simple designs |
| CustomErrorBuilder.snackbar() | Non-blocking errors |
| CustomErrorBuilder.custom() | Custom widgets |
Automatic Retry #
SuperPaginationCubit<Product, SuperPaginationRequest>(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Product, SuperPaginationRequest>.future(fetchProducts),
retryConfig: RetryConfig(
maxAttempts: 3,
initialDelay: Duration(seconds: 1),
shouldRetry: (error) => error is NetworkException,
),
)
Data Operations #
Programmatically manipulate items through the cubit. All methods return Future<bool> indicating success.
Insert #
await cubit.insertEmit(newProduct); // insert at index 0
await cubit.insertAllEmit([product1, product2], index: 0); // insert multiple
await cubit.addOrUpdateEmit(product); // add if new, update if exists
Remove #
await cubit.removeItemEmit(product); // remove by value
await cubit.removeAtEmit(2); // remove at index
await cubit.removeWhereEmit((p) => p.stock == 0); // remove all matches
await cubit.removeFirstWhereEmit((p) => p.stock == 0); // remove first match
await cubit.removeLastWhereEmit((p) => p.isArchived); // remove last match
Update #
Applies a transform function to the matched item(s).
await cubit.updateItemEmit(
(p) => p.id == productId,
(p) => p.copyWith(price: newPrice),
); // first match
await cubit.updateWhereEmit(
(p) => p.category == 'sale',
(p) => p.copyWith(discount: 0.2),
); // all matches
await cubit.updateFirstWhereEmit(
(p) => p.isPinned,
(p) => p.copyWith(isPinned: false),
); // explicitly first match
await cubit.updateLastWhereEmit(
(p) => p.isPinned,
(p) => p.copyWith(isPinned: false),
); // last match
await cubit.updateAtEmit(0, (p) => p.copyWith(isFeatured: true)); // at index
Replace #
Swaps the matched item with a new instance directly (no updater function).
await cubit.replaceFirstWhereEmit((p) => p.id == id, updatedProduct); // first match
await cubit.replaceLastWhereEmit((p) => p.isDraft, publishedProduct); // last match
await cubit.replaceAtEmit(3, newProduct); // at index
Refresh #
Async re-fetch of a specific item from the server.
await cubit.refreshItem(
(p) => p.id == productId,
(p) => api.fetchProduct(p.id),
); // first match
await cubit.refreshFirstWhereEmit(
(p) => p.isStale,
(p) => api.fetchProduct(p.id),
); // explicitly first match
await cubit.refreshLastWhereEmit(
(p) => p.isStale,
(p) => api.fetchProduct(p.id),
); // last match
await cubit.refreshAtEmit(0, (p) => api.fetchProduct(p.id)); // at index
Bulk #
await cubit.clearItems();
await cubit.setItems(customList);
cubit.reload();
Partial Updates & Animations #
Enable targeted UI updates instead of full list rebuilds by providing itemKeyBuilder:
SuperPaginationListView.withCubit(
cubit: cubit,
itemKeyBuilder: (item, index) => item.id,
insertItemAnimationBuilder: (context, index, animation, child) {
return SlideTransition(
position: Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero)
.animate(animation),
child: child,
);
},
removeItemAnimationBuilder: (context, index, animation, child) {
return FadeTransition(opacity: animation, child: child);
},
animationDuration: const Duration(milliseconds: 400),
itemBuilder: (context, items, index) => ProductTile(product: items[index]),
);
ListView uses SliverAnimatedList for insert/remove animations. Other view types (GridView, PageView, etc.) use key-based widget reconciliation for efficient partial updates without animations.
Sorting #
final orders = SortOrderCollection<Product>(
orders: [
SortOrder.byField(id: 'name', label: 'Name', fieldSelector: (p) => p.name),
SortOrder.byField(id: 'price', label: 'Price', fieldSelector: (p) => p.price),
],
defaultOrderId: 'name',
);
final cubit = SuperPaginationCubit<Product, SuperPaginationRequest>(
request: SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Product, SuperPaginationRequest>.future(fetchProducts),
orders: orders,
);
// Change sort
cubit.setActiveOrder('price');
cubit.resetOrder();
Data Providers #
Future (REST API) #
SuperPaginationProvider.future((request) => api.fetchProducts(request))
Stream (Real-time) #
SuperPaginationProvider.stream((request) => firestore.collection('products').snapshots())
Stream Accumulation
Stream pagination accumulates per-page subscriptions within a pagination scope. When a user loads page 2, the page 1 stream stays subscribed; when they load page 3, page 2 stays subscribed too. Emissions on any active page update only that page's slice; the merged list always reflects page1 ∪ page2 ∪ … ∪ pageN in page order.
The cubit owns one subscription per page in an internal registry. All accumulated subscriptions are cancelled together on a scope reset: refresh, reload, filter change, search-query change, provider replacement, page eviction (via maxPagesInMemory), or cubit dispose. Stale emissions buffered before a reset are dropped via a generation token.
End-of-pagination semantics
A page whose latest emission has fewer items than pageSize is treated as the end of pagination. While at least one active page is in this state the cubit rejects subsequent loadMore() calls. The rule is dynamic: if a later emission grows a partial page back to a full page, loadMore() is re-enabled in the same scope. An empty list [] emission is honoured — it clears that page's slice and triggers end-of-pagination, just like any other partial emission.
Merged Streams #
SuperPaginationProvider.mergeStreams((request) => [
regularStream(request),
featuredStream(request),
])
The merged provider supports zero, one, or many input streams. The single-stream case is wrapped in a controller for lifecycle symmetry: cancelling the merged subscription cancels every underlying child subscription. The merged stream completes only when every child has completed.
Per-Page Error Annotation #
When a stream provider's page errors, the cubit isolates the failure to that page rather than transitioning to a global error state. The failing page's subscription is cancelled, sibling pages keep emitting, and the page's last good slice remains in the merged view alongside an entry in state.pageErrors:
BlocBuilder<SuperPaginationCubit<Product, ProductRequest>, SuperPaginationState<Product>>(
builder: (context, state) {
if (state is! SuperPaginationLoaded<Product>) return const SizedBox.shrink();
return Column(
children: [
if (state.pageErrors.isNotEmpty)
MaterialBanner(
content: Text('Failed to refresh ${state.pageErrors.length} page(s)'),
actions: const [SizedBox.shrink()],
),
Expanded(child: ProductList(items: state.items)),
],
);
},
)
state.pageErrors is a Map<int, Object> keyed by 1-based page index. Empty when no per-page error is in flight. A successful subsequent emission on the same page (after re-subscribing via a refresh) clears its annotation.
Common Parameters #
| Parameter | Type | Description |
|-----------|------|-------------|-|
| request | SuperPaginationRequest | Page number and size |
| provider | SuperPaginationProvider<T, SuperPaginationRequest> | Data source |
| itemBuilder | Widget Function(context, items, index) | Item widget builder |
| invisibleItemsThreshold | int | Preload trigger (default: 3) |
| separator | Widget? | Divider between items |
| scrollController | ScrollController? | Custom scroll controller |
| shrinkWrap | bool | Fit content size |
| reverse | bool | Reverse scroll direction |
| canRefresh | bool | Enable built-in pull-to-refresh (default: false) |
| onRefresh | Future<void> Function(cubit)? | Custom refresh callback (default: cubit.reload()) |
Built-in Pull to Refresh #
SuperPaginationListView.withProvider(
request: const SuperPaginationRequest(page: 1, pageSize: 20),
provider: SuperPaginationProvider<Product, SuperPaginationRequest>.future(fetchProducts),
canRefresh: true,
onRefresh: (cubit) async {
cubit.reload();
},
itemBuilder: (context, items, index) => ProductTile(product: items[index]),
)
State Builders #
| Parameter | Description |
|-----------|-------------|-|
| firstPageLoadingBuilder | Initial loading widget |
| firstPageErrorBuilder | Initial error widget |
| firstPageEmptyBuilder | Empty state widget |
| loadMoreLoadingBuilder | Bottom loading indicator |
| loadMoreErrorBuilder | Pagination error widget |
| loadMoreNoMoreItemsBuilder | End of list widget |
Cubit API #
final cubit = SuperPaginationCubit<T, SuperPaginationRequest>({
required SuperPaginationRequest request,
required SuperPaginationProvider<T, SuperPaginationRequest> provider,
RetryConfig? retryConfig,
Duration? dataAge,
int? maxPagesInMemory,
SortOrderCollection<T>? orders,
});
Properties #
cubit.currentItems; // List<T>
cubit.isDataExpired; // bool
cubit.lastFetchTime; // DateTime?
cubit.activeOrder; // SortOrder<T>?
cubit.didFetch; // bool
Pagination Control #
cubit.fetchPaginatedList();
cubit.refreshPaginatedList();
cubit.filterPaginatedList(test);
cubit.cancelOngoingRequest();
cubit.reload();
Insert #
| Method | Description |
|--------|-------------|-|
| insertEmit(item, {index}) | Insert single item |
| insertAllEmit(items, {index}) | Insert multiple items |
| addOrUpdateEmit(item, {index}) | Add if new, update if exists |
Remove #
| Method | Description |
|--------|-------------|-|
| removeItemEmit(item) | Remove by value |
| removeAtEmit(index) | Remove at index |
| removeWhereEmit(test) | Remove all matching |
| removeFirstWhereEmit(test) | Remove first matching |
| removeLastWhereEmit(test) | Remove last matching |
Update #
| Method | Description |
|--------|-------------|-|
| updateItemEmit(matcher, updater) | Update first matching item |
| updateWhereEmit(matcher, updater) | Update all matching items |
| updateFirstWhereEmit(matcher, updater) | Update first matching item (explicit) |
| updateLastWhereEmit(matcher, updater) | Update last matching item |
| updateAtEmit(index, updater) | Update item at index |
Replace #
| Method | Description |
|--------|-------------|-|
| replaceFirstWhereEmit(matcher, item) | Replace first matching item |
| replaceLastWhereEmit(matcher, item) | Replace last matching item |
| replaceAtEmit(index, item) | Replace item at index |
Refresh (async, re-fetches from server) #
| Method | Description |
|--------|-------------|-|
| refreshItem(matcher, refresher) | Refresh first matching item |
| refreshFirstWhereEmit(matcher, refresher) | Refresh first matching item (explicit) |
| refreshLastWhereEmit(matcher, refresher) | Refresh last matching item |
| refreshAtEmit(index, refresher) | Refresh item at index |
Bulk #
| Method | Description |
|--------|-------------|-|
| setItems(items) | Replace entire list |
| clearItems() | Remove all items |
Sorting #
cubit.setActiveOrder(orderId);
cubit.resetOrder();
Scroll Navigation #
// Attach observer for precise navigation
cubit.attachListObserverController(observerController);
// Navigate
await cubit.animateToIndex(index, alignment: 0.5);
cubit.jumpToIndex(index);
await cubit.animateFirstWhere((item) => item.id == targetId);
cubit.jumpFirstWhere((item) => item.isUnread);
Theming #
Install SuperMaterialThemeData once. Pagination states and search surfaces then
read the active GeniusLink palette, brightness, typography, spacing, radii, and
responsive mode automatically.
MaterialApp(
theme: SuperMaterialThemeData.light(
palette: SuperPalette.bluePalette,
),
darkTheme: SuperMaterialThemeData.dark(
palette: SuperPalette.bluePalette,
),
)
Use the package extensions only for focused overrides. Their default factories
are still generated from the same super_core palette:
SuperMaterialThemeData.light(
palette: SuperPalette.greenPalette,
extensions: [
SuperSearchTheme.light(
palette: SuperPalette.greenPalette,
).copyWith(searchBoxElevation: 0),
SuperPaginationTheme.light(
palette: SuperPalette.greenPalette,
).copyWith(errorTitleColor: SuperTokens.danger),
],
)
Example App #
The example app preserves all existing demonstrations and typed
routes while using a feature-first Clean Architecture layout. Application
bootstrap, routing, theme control, domain entities, application contracts,
infrastructure adapters, controllers, and views are separated explicitly.
Legacy example paths remain available as compatibility exports. The application
chrome is provided by super_navigation_sidebar: the same typed route tree
adapts to expanded desktop navigation, a tablet rail, and a mobile drawer while
TypedShellRoute keeps the nested navigator alive.
cd example
flutter pub get
flutter run
The example includes pagination, stream, search, error-handling, and Firebase
integration demonstrations. See example/ARCHITECTURE.md for the dependency
rules and example/REFACTORING_REPORT_AR.md for the Arabic review.
Best Practices #
1. Reuse cubits - Create once in initState, dispose in dispose
2. Use state separation - Different UI for first-page vs load-more errors
3. Configure preloading - Adjust invisibleItemsThreshold for your scroll speed
4. Set memory limits - Use maxPagesInMemory for large datasets
5. Use key-based selection - For forms and state management in search dropdowns
Resources #
License #
MIT License - see LICENSE
Transport agnostic - Bring your own async function
Made by Genius Systems