
Production-ready Flutter pagination with built-in BLoC, search dropdowns, and error handling.
SmartPaginationListView.withProvider(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future((req) => api.getProducts(req)),
itemBuilder: (context, items, index) => ProductTile(items[index]),
)
Features
- 7 Widget Classes - ListView, GridView, PageView, StaggeredGrid, Column, Row, ReorderableList
- Smart 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 items programmatically
- Auto Expiration - Configurable data age for global cubits
Installation
dependencies:
smart_pagination: ^3.1.0
import 'package:smart_pagination/pagination.dart';
Quick Start
ListView
SmartPaginationListView.withProvider(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future((req) => fetchProducts(req)),
itemBuilder: (context, items, index) => ListTile(
title: Text(items[index].name),
),
)
GridView
SmartPaginationGridView.withProvider(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future((req) => fetchProducts(req)),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context, items, index) => ProductCard(items[index]),
)
With External Cubit
final cubit = SmartPaginationCubit<Product>(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future(fetchProducts),
dataAge: Duration(minutes: 5), // Auto-refresh stale data
);
SmartPaginationListView.withCubit(
cubit: cubit,
itemBuilder: (context, items, index) => ProductTile(items[index]),
)
| Widget |
Layout |
Use Case |
SmartPaginationListView |
Vertical/horizontal list |
Feeds, messages |
SmartPaginationGridView |
Multi-column grid |
Catalogs, galleries |
SmartPaginationColumn |
Non-scrollable column |
Embedded in ScrollView |
SmartPaginationRow |
Non-scrollable row |
Chips, tags |
SmartPaginationPageView |
Swipeable pages |
Onboarding, carousels |
SmartPaginationStaggeredGridView |
Masonry layout |
Pinterest-style |
SmartPaginationReorderableListView |
Drag-and-drop |
Task lists |
Each widget has two constructors:
.withProvider(...) - Creates cubit internally
.withCubit(...) - Uses external cubit
Smart Search
Search components with auto-positioning overlay and key-based selection.
Basic Dropdown
SmartSearchDropdown<Product, int>.withProvider(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future((req) => api.search(req.searchQuery)),
searchRequestBuilder: (query) => PaginationRequest(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.
SmartSearchDropdown<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
SmartSearchMultiDropdown<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:
SmartSearchMultiDropdown<Product, int>.withProvider(
// ... provider config
displayMode: SearchDisplayMode.bottomSheet,
bottomSheetConfig: SmartSearchBottomSheetConfig(
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 |
SmartSearchDropdown<T, K> |
Single-selection search dropdown |
SmartSearchMultiDropdown<T, K> |
Multi-selection with chips |
SmartSearchController<T, K> |
Controller for programmatic control |
SmartSearchBox<T, K> |
Standalone search input |
SmartSearchOverlay<T, K> |
Standalone results overlay |
SmartSearchTheme |
ThemeExtension for styling |
Configuration
SmartSearchDropdown<Product, int>.withProvider(
// ...
searchConfig: SmartSearchConfig(
debounceDelay: Duration(milliseconds: 500),
minSearchLength: 2,
searchOnEmpty: false,
),
overlayConfig: SmartSearchOverlayConfig(
position: OverlayPosition.auto,
maxHeight: 400,
animationType: OverlayAnimationType.fadeScale,
),
)
SmartSearchDropdown Parameters
Core Parameters
| Parameter |
Type |
Required |
Default |
Description |
request |
PaginationRequest |
Yes* |
- |
Pagination config (for .withProvider) |
provider |
PaginationProvider<T> |
Yes* |
- |
Data source (for .withProvider) |
cubit |
SmartPaginationCubit<T> |
Yes* |
- |
External cubit (for .withCubit) |
searchRequestBuilder |
PaginationRequest 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 |
| 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 |
SmartSearchConfig |
No |
SmartSearchConfig() |
Search behavior config |
overlayConfig |
SmartSearchOverlayConfig |
No |
SmartSearchOverlayConfig() |
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 |
SmartSearchMultiDropdown Parameters
Core Parameters
| Parameter |
Type |
Required |
Default |
Description |
request |
PaginationRequest |
Yes* |
- |
Pagination config (for .withProvider) |
provider |
PaginationProvider<T> |
Yes* |
- |
Data source (for .withProvider) |
cubit |
SmartPaginationCubit<T> |
Yes* |
- |
External cubit (for .withCubit) |
searchRequestBuilder |
PaginationRequest 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(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 |
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 |
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 |
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 |
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 |
| 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 |
SmartSearchConfig |
No |
SmartSearchConfig() |
Search behavior config |
overlayConfig |
SmartSearchOverlayConfig |
No |
SmartSearchOverlayConfig() |
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 |
Error Handling
Separate First-Page and Load-More Errors
SmartPaginationListView.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
SmartPaginationCubit<Product>(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future(fetchProducts),
retryConfig: RetryConfig(
maxAttempts: 3,
initialDelay: Duration(seconds: 1),
shouldRetry: (error) => error is NetworkException,
),
)
Data Operations
Programmatically manipulate items through the cubit.
// Insert
cubit.insertEmit(newProduct);
cubit.insertAllEmit([product1, product2], index: 0);
// Remove
cubit.removeItemEmit(product);
cubit.removeAtEmit(index);
cubit.removeWhereEmit((item) => item.stock == 0);
// Update
cubit.updateItemEmit(
(item) => item.id == productId,
(item) => item.copyWith(price: newPrice),
);
// Other
cubit.clearItems();
cubit.reload();
cubit.setItems(customList);
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 = SmartPaginationCubit<Product>(
request: PaginationRequest(page: 1, pageSize: 20),
provider: PaginationProvider.future(fetchProducts),
orders: orders,
);
// Change sort
cubit.setActiveOrder('price');
cubit.resetOrder();
Data Providers
Future (REST API)
PaginationProvider.future((request) => api.fetchProducts(request))
Stream (Real-time)
PaginationProvider.stream((request) => firestore.collection('products').snapshots())
Merged Streams
PaginationProvider.mergeStreams((request) => [
regularStream(request),
featuredStream(request),
])
Common Parameters
| Parameter |
Type |
Description |
request |
PaginationRequest |
Page number and size |
provider |
PaginationProvider<T> |
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 |
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 = SmartPaginationCubit<T>({
required PaginationRequest request,
required PaginationProvider<T> 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>?
// Methods
cubit.fetchPaginatedList();
cubit.reload();
cubit.insertEmit(item);
cubit.removeItemEmit(item);
cubit.updateItemEmit(matcher, updater);
cubit.setActiveOrder(orderId);
// 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
Use standard Flutter theming with custom loading/empty/error builders.
Search Theme
MaterialApp(
theme: ThemeData.light().copyWith(
extensions: [SmartSearchTheme.light()],
),
darkTheme: ThemeData.dark().copyWith(
extensions: [SmartSearchTheme.dark()],
),
)
Custom theme:
SmartSearchTheme(
searchBoxBackgroundColor: Colors.grey[100],
searchBoxBorderRadius: BorderRadius.circular(12),
overlayBackgroundColor: Colors.white,
overlayElevation: 8,
itemHoverColor: Colors.grey[100],
itemFocusedColor: Colors.blue.withOpacity(0.1),
)
Example App
The example app includes 29+ demonstration screens covering all features.
cd example
flutter pub get
flutter run
Categories:
- Basic pagination (ListView, GridView, filters)
- Stream examples (single, multi, merged)
- Error handling (all 6 styles, recovery strategies)
- Advanced (scroll control, staggered grid, reorderable)
- Smart Search (key-based selection, multi-select)
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