Pullable
Introduction
The Pullable widget adds pull-to-refresh and load-more functionality to any scrollable content. It wraps your child widget with gesture-driven refresh and pagination behavior, supporting multiple header animation styles.
Built on top of the pull_to_refresh_flutter3 package, Pullable provides a clean API with named constructors for common configurations.
Pullable(
onRefresh: () async {
// Fetch fresh data
await fetchData();
},
child: ListView(
children: items.map((item) => ListTile(title: Text(item))).toList(),
),
)
Basic Usage
Wrap any scrollable widget with Pullable:
Pullable(
onRefresh: () async {
await loadLatestPosts();
},
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => PostCard(post: posts[index]),
),
)
When the user pulls down on the list, the onRefresh callback fires. The refresh indicator automatically completes when the callback finishes.
Constructors
Pullable provides named constructors for common configurations:
| Constructor | Header Style | Description |
|---|---|---|
Pullable() |
Water Drop | Default constructor |
Pullable.classicHeader() |
Classic | Classic pull-to-refresh style |
Pullable.waterDropHeader() |
Water Drop | Water drop animation |
Pullable.materialClassicHeader() |
Material Classic | Material Design classic style |
Pullable.waterDropMaterialHeader() |
Water Drop Material | Material water drop style |
Pullable.bezierHeader() |
Bezier | Bezier curve animation |
Pullable.noBounce() |
Configurable | Reduced bounce with ClampingScrollPhysics |
Pullable.custom() |
Custom widget | Use your own header/footer widgets |
Pullable.builder() |
Configurable | Full PullableConfig control |
Examples
// Classic header
Pullable.classicHeader(
onRefresh: () async => await refreshData(),
child: myListView,
)
// Material header
Pullable.materialClassicHeader(
onRefresh: () async => await refreshData(),
child: myListView,
)
// No bounce effect
Pullable.noBounce(
onRefresh: () async => await refreshData(),
headerType: PullableHeaderType.classic,
child: myListView,
)
// Custom header widget
Pullable.custom(
customHeader: MyCustomRefreshHeader(),
onRefresh: () async => await refreshData(),
child: myListView,
)
PullableConfig
For fine-grained control, use PullableConfig with the Pullable.builder() constructor:
Pullable.builder(
config: PullableConfig(
enablePullDown: true,
enablePullUp: true,
headerType: PullableHeaderType.materialClassic,
onRefresh: () async => await refreshData(),
onLoading: () async => await loadMoreData(),
refreshCompleteDelay: Duration(milliseconds: 500),
loadCompleteDelay: Duration(milliseconds: 300),
physics: BouncingScrollPhysics(),
),
child: myListView,
)
All Configuration Options
| Property | Type | Default | Description |
|---|---|---|---|
enablePullDown |
bool |
true |
Enable pull-down-to-refresh |
enablePullUp |
bool |
false |
Enable pull-up-to-load-more |
physics |
ScrollPhysics? |
null | Custom scroll physics |
onRefresh |
Future<void> Function()? |
null | Refresh callback |
onLoading |
Future<void> Function()? |
null | Load-more callback |
headerType |
PullableHeaderType |
waterDrop |
Header animation style |
customHeader |
Widget? |
null | Custom header widget |
customFooter |
Widget? |
null | Custom footer widget |
refreshCompleteDelay |
Duration |
Duration.zero |
Delay before refresh completes |
loadCompleteDelay |
Duration |
Duration.zero |
Delay before load completes |
enableOverScroll |
bool |
true |
Allow over-scroll effect |
cacheExtent |
double? |
null | Scroll cache extent |
semanticChildCount |
int? |
null | Semantic children count |
dragStartBehavior |
DragStartBehavior |
start |
How drag gestures begin |
Header Styles
Choose from five built-in header animations:
enum PullableHeaderType {
classic, // Classic pull indicator
waterDrop, // Water drop animation (default)
materialClassic, // Material Design classic
waterDropMaterial, // Material water drop
bezier, // Bezier curve animation
}
Set the style via the constructor or config:
// Via named constructor
Pullable.bezierHeader(
onRefresh: () async => await refreshData(),
child: myListView,
)
// Via config
Pullable.builder(
config: PullableConfig(
headerType: PullableHeaderType.bezier,
onRefresh: () async => await refreshData(),
),
child: myListView,
)
Pull-Up to Load More
Enable pagination with pull-up loading:
Pullable.builder(
config: PullableConfig(
enablePullDown: true,
enablePullUp: true,
onRefresh: () async {
// Reset to page 1
page = 1;
items = await fetchItems(page: page);
setState(() {});
},
onLoading: () async {
// Load next page
page++;
List<Item> more = await fetchItems(page: page);
items.addAll(more);
setState(() {});
},
),
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
),
)
Custom Headers and Footers
Provide your own header and footer widgets:
Pullable.custom(
customHeader: Container(
height: 60,
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
customFooter: Container(
height: 40,
alignment: Alignment.center,
child: Text("Loading more..."),
),
enablePullUp: true,
onRefresh: () async => await refreshData(),
onLoading: () async => await loadMore(),
child: myListView,
)
Controller
Use a RefreshController for programmatic control:
final RefreshController _controller = RefreshController();
Pullable(
controller: _controller,
onRefresh: () async => await refreshData(),
child: myListView,
)
// Trigger refresh programmatically
_controller.triggerRefresh();
// Trigger loading programmatically
_controller.triggerLoading();
// Check state
bool refreshing = _controller.isRefreshing;
bool loading = _controller.isLoading;
Extension Methods on RefreshController
| Method/Getter | Return Type | Description |
|---|---|---|
triggerRefresh() |
void |
Manually trigger a refresh |
triggerLoading() |
void |
Manually trigger load-more |
isRefreshing |
bool |
Whether refresh is active |
isLoading |
bool |
Whether loading is active |
Extension Method
Any widget can be wrapped with pull-to-refresh using the .pullable() extension:
ListView(
children: items.map((item) => ListTile(title: Text(item.name))).toList(),
).pullable(
onRefresh: () async {
await fetchItems();
},
)
With custom config:
myListView.pullable(
onRefresh: () async => await refreshData(),
pullableConfig: PullableConfig(
headerType: PullableHeaderType.classic,
enablePullUp: true,
onLoading: () async => await loadMore(),
),
)
CollectionView Integration
CollectionView provides pullable variants with built-in pagination:
CollectionView.pullable
CollectionView<User>.pullable(
data: (iteration) async => api.getUsers(page: iteration),
builder: (context, item) => UserTile(user: item.data),
onRefresh: () => print('Refreshed!'),
headerStyle: 'WaterDropHeader',
)
CollectionView.pullableSeparated
CollectionView<User>.pullableSeparated(
data: (iteration) async => api.getUsers(page: iteration),
builder: (context, item) => UserTile(user: item.data),
separatorBuilder: (context, index) => Divider(),
)
CollectionView.pullableGrid
CollectionView<Product>.pullableGrid(
data: (iteration) async => api.getProducts(page: iteration),
builder: (context, item) => ProductCard(product: item.data),
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
)
Pullable-Specific Parameters
| Parameter | Type | Description |
|---|---|---|
data |
Function(int iteration) |
Paginated data callback (iteration starts at 1) |
onRefresh |
Function()? |
Callback after refresh |
beforeRefresh |
Function()? |
Hook before refresh begins |
afterRefresh |
Function(dynamic)? |
Hook after refresh with data |
headerStyle |
String? |
Header type name (e.g., 'WaterDropHeader', 'ClassicHeader') |
footerLoadingIcon |
Widget? |
Custom loading indicator for footer |
Examples
Paginated List with Refresh
class _PostListState extends NyState<PostListPage> {
List<Post> posts = [];
int page = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Pullable.builder(
config: PullableConfig(
enablePullDown: true,
enablePullUp: true,
headerType: PullableHeaderType.materialClassic,
onRefresh: () async {
page = 1;
posts = await api<PostApiService>((request) => request.getPosts(page: page));
setState(() {});
},
onLoading: () async {
page++;
List<Post> more = await api<PostApiService>((request) => request.getPosts(page: page));
posts.addAll(more);
setState(() {});
},
),
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => PostCard(post: posts[index]),
),
),
);
}
}
Simple Refresh with Extension
ListView(
children: notifications
.map((n) => ListTile(
title: Text(n.title),
subtitle: Text(n.body),
))
.toList(),
).pullable(
onRefresh: () async {
notifications = await fetchNotifications();
setState(() {});
},
)