Any Refreshable Widget
A powerful and flexible Flutter package that provides pull-to-refresh functionality for any widget, with support for single and multiple futures, custom indicators, and comprehensive error handling.
Features
- π Single & Multiple Future Support - Handle one or multiple asynchronous operations
- π¨ Customizable Refresh Indicator - Full control over appearance and behavior
- π± Universal Widget Support - Works with any widget, automatically makes content scrollable
- π― Smart Error Handling - Comprehensive error states and callbacks
- π§ Highly Configurable - Colors, displacement, stroke width, trigger modes, and more
- β‘ Lifecycle Callbacks -
onBeforeRefresh
andonAfterRefresh
hooks with sync/async support - π Flexible Concurrency - Choose between concurrent (parallel) or sequential execution
- π Production Ready - Thoroughly tested and optimized for real-world applications
Installation
Add this to your package's pubspec.yaml
file:
dependencies:
any_refreshable_widget: ^0.0.1
Then run:
flutter pub get
Quick Start
Import the package
import 'package:any_refreshable_widget/any_refreshable_widget.dart';
Basic Usage - Single Future
AnyRefreshableWidget.single(
onRefresh: () async {
// Your refresh logic here
await fetchUserData();
},
builder: (context, isLoading, error) {
if (error != null) {
return Center(child: Text('Error: $error'));
}
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return const Center(child: Text('Pull down to refresh!'));
},
)
Advanced Usage - Multiple Futures
AnyRefreshableWidget(
onRefresh: [
() => fetchUserData(),
() => fetchNotifications(),
() => fetchSettings(),
],
builder: (context, isLoading, error) {
if (error != null) {
return ErrorWidget(error);
}
if (isLoading) {
return const LoadingWidget();
}
return const ContentWidget();
},
)
Concurrency Control
Control how multiple futures are executed:
Concurrent Execution (Default)
AnyRefreshableWidget(
concurrency: RefreshConcurrency.concurrent,
onRefresh: [
() => fetchUserData(), // These run simultaneously
() => fetchNotifications(), // for faster completion
() => fetchSettings(),
],
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Sequential Execution
AnyRefreshableWidget(
concurrency: RefreshConcurrency.sequential, // Default
onRefresh: [
() => authenticateUser(), // Runs first
() => fetchUserData(), // Then this
() => fetchNotifications(), // Finally this
],
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Comprehensive Examples
Custom Refresh Indicator
AnyRefreshableWidget.single(
onRefresh: () => performRefresh(),
refreshColor: Colors.blue,
backgroundColor: Colors.white,
displacement: 60.0,
strokeWidth: 3.0,
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Custom Indicator Widget
AnyRefreshableWidget.single(
onRefresh: () => performRefresh(),
customIndicator: Container(
padding: const EdgeInsets.all(16),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(strokeWidth: 2),
SizedBox(width: 16),
Text('Refreshing...'),
],
),
),
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Error Handling
AnyRefreshableWidget.single(
onRefresh: () async {
// Simulate potential error
if (Random().nextBool()) {
throw Exception('Network error occurred');
}
await fetchData();
},
builder: (context, isLoading, error) {
if (error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text('Error: ${error.toString()}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Trigger refresh programmatically
},
child: const Text('Retry'),
),
],
),
);
}
if (isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading...'),
],
),
);
}
return YourDataWidget();
},
)
With ListView
AnyRefreshableWidget.single(
onRefresh: () => refreshListData(),
builder: (context, isLoading, error) {
if (error != null) return ErrorWidget(error);
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].title),
subtitle: Text(items[index].description),
);
},
);
},
)
Lifecycle Callbacks
The package supports onBeforeRefresh
and onAfterRefresh
callbacks that can be either synchronous or asynchronous:
Synchronous Callbacks
AnyRefreshableWidget.single(
onBeforeRefresh: () {
print('Starting refresh...');
// Synchronous setup logic
},
onRefresh: () => fetchData(),
onAfterRefresh: () {
print('Refresh completed!');
// Synchronous cleanup logic
},
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Asynchronous Callbacks
AnyRefreshableWidget.single(
onBeforeRefresh: () async {
print('Starting refresh...');
await prepareForRefresh();
// Asynchronous setup logic
},
onRefresh: () => fetchData(),
onAfterRefresh: () {
print('Refresh completed!');
// Cleanup logic (always sync)
},
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
API Reference
AnyRefreshableWidget
Parameter | Type | Required | Default | Description |
---|---|---|---|---|
onRefresh |
List<Future<void> Function()> |
β | - | List of async functions to execute on refresh |
builder |
Widget Function(BuildContext, bool, Object?) |
β | - | Builder function with loading and error states |
concurrency |
RefreshConcurrency |
β | concurrent |
How futures should be executed (concurrent/sequential) |
onBeforeRefresh |
FutureOr<void> Function()? |
β | null |
Callback executed before refresh starts (sync/async) |
onAfterRefresh |
VoidCallback? |
β | null |
Callback executed after refresh completes |
refreshColor |
Color? |
β | null |
Color of the refresh indicator |
backgroundColor |
Color? |
β | null |
Background color of the refresh indicator |
displacement |
double |
β | 40.0 |
Distance from top to show indicator |
strokeWidth |
double |
β | 2.0 |
Stroke width of the progress indicator |
customIndicator |
Widget? |
β | null |
Custom refresh indicator widget |
triggerMode |
RefreshIndicatorTriggerMode? |
β | anywhere |
When the indicator should trigger |
notificationPredicate |
bool Function(ScrollNotification)? |
β | null |
Custom scroll notification predicate |
AnyRefreshableWidget.single
Same parameters as AnyRefreshableWidget
, but onRefresh
takes a single Future<void> Function()
instead of a list. The concurrency
parameter is not applicable for single futures.
RefreshConcurrency Enum
Value | Description | Use Case |
---|---|---|
RefreshConcurrency.concurrent |
Execute all futures simultaneously using Future.wait |
fastest refresh when futures are independent |
RefreshConcurrency.sequential |
Execute futures one by one in order | When futures depend on each other or to limit resource usage |
Callback Execution Order
When a refresh is triggered, the callbacks execute in this order:
onBeforeRefresh
- Called first, awaited if async- Loading state -
isLoading
becomestrue
, UI updates onRefresh
- All futures execute concurrently- Loading state -
isLoading
becomesfalse
, UI updates onAfterRefresh
- Called last, always synchronous
Advanced Configuration
Custom Scroll Notification Predicate
AnyRefreshableWidget.single(
onRefresh: () => performRefresh(),
notificationPredicate: (ScrollNotification notification) {
// Custom logic to determine when refresh should trigger
return notification.depth == 0 && notification.metrics.pixels <= 0;
},
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Trigger Modes
AnyRefreshableWidget.single(
onRefresh: () => performRefresh(),
triggerMode: RefreshIndicatorTriggerMode.onEdge, // or .anywhere
builder: (context, isLoading, error) {
return YourContentWidget();
},
)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Issues
If you encounter any issues or have suggestions, please file them in the GitHub Issues.
Changelog
See CHANGELOG.md for a detailed changelog.
Made with β€οΈ by Yama-Roni