π¦ pub.dev: https://pub.dev/packages/smart_scroll
A powerful Flutter widget for pull-to-refresh and infinite scroll (load more / pagination).
Works seamlessly with ListView, GridView, CustomScrollView, and any scrollable widget.
Reuse flutter_pulltorefresh by peng8350, due to issue related to updating version dart.
βοΈ Pull down to refresh & pull up to load more- π¨ Multiple built-in indicators β Material, Cupertino, WaterDrop, Bezier, Classic, Shimmer
- π Platform-adaptive headers/footers β
PlatformHeader&PlatformFooterauto-detect iOS/Android - π οΈ Custom indicator builder β full control with
BuilderHeader/BuilderFooter - π Horizontal & vertical scrolling, including reverse scroll direction
- βοΈ Global configuration via
RefreshConfiguration(shared across all pages) - π Localization support for indicator text
- π± Cross-platform: iOS, Android, Web, Desktop
Add to your pubspec.yaml:
dependencies:
smart_scroll: ^1.0.2Then run:
flutter pub getimport 'package:smart_scroll/smart_scroll.dart';
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final RefreshController _refreshController =
RefreshController(initialRefresh: false);
List<String> items = List.generate(10, (i) => 'Item ${i + 1}');
void _onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
_refreshController.refreshCompleted();
}
void _onLoading() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
items.add('Item ${items.length + 1}');
});
_refreshController.loadComplete();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SmartScroll(
enablePullDown: true,
enablePullUp: true,
header: const WaterDropHeader(),
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(title: Text(items[index])),
),
),
);
}
}| Classic | WaterDrop | Material |
|---|---|---|
![]() |
![]() |
![]() |
| Material WaterDrop | Shimmer | Bezier + Circle |
|---|---|---|
![]() |
![]() |
![]() |
| RefreshStyle.Follow | RefreshStyle.UnFollow | RefreshStyle.Behind | RefreshStyle.Front |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| LoadStyle.ShowAlways | LoadStyle.HideAlways | LoadStyle.ShowWhenLoading |
|---|---|---|
![]() |
![]() |
![]() |
| Basic | Link Header | Reverse + Horizontal |
|---|---|---|
![]() |
![]() |
![]() |
| Two Level | Other Widgets | Chat List |
|---|---|---|
![]() |
![]() |
![]() |
| Custom Header (SpinKit) | DraggableSheet + LoadMore | GIF Indicator |
|---|---|---|
![]() |
![]() |
![]() |
Use RefreshConfiguration at the root of your app to set defaults for all SmartScroll widgets:
RefreshConfiguration(
headerBuilder: () => const WaterDropHeader(),
footerBuilder: () => const ClassicFooter(),
headerTriggerDistance: 80.0,
maxOverScrollExtent: 100,
maxUnderScrollExtent: 0,
enableScrollWhenRefreshCompleted: true,
enableLoadingWhenFailed: true,
hideFooterWhenNotFull: false,
enableBallisticLoad: true,
child: MaterialApp(
// ...
),
);Build fully custom refresh/load indicators using BuilderHeader and BuilderFooter:
SmartScroll(
header: BuilderHeader(
builder: (context, data) {
return Container(
height: 60,
alignment: Alignment.center,
child: Text(
data.triggered ? 'Release to refresh' : 'Pull down...',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
);
},
),
// ...
);MaterialApp(
localizationsDelegates: [
RefreshLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('zh'),
],
);SmartScroll works best when its child is a direct ScrollView (ListView, GridView, CustomScrollView).
// β
Correct
ScrollBar(
child: SmartScroll(
child: ListView(...),
),
)
// β Wrong β don't wrap ScrollView before passing to SmartScroll
SmartScroll(
child: ScrollBar(
child: ListView(...),
),
)// β Wrong β don't hide ScrollView inside another widget
SmartScroll(
child: MyCustomWidget(), // contains ListView internally
)| Property | Type | Description |
|---|---|---|
child |
Widget |
The scrollable content (ListView, GridView, etc.) |
controller |
RefreshController |
Controls refresh/load state |
header |
Widget? |
Custom refresh header indicator |
footer |
Widget? |
Custom load-more footer indicator |
enablePullDown |
bool |
Enable pull-down-to-refresh (default: true) |
enablePullUp |
bool |
Enable pull-up-to-load-more (default: false) |
onRefresh |
VoidCallback? |
Called when pull-down refresh is triggered |
onLoading |
VoidCallback? |
Called when pull-up load-more is triggered |
| Method | Description |
|---|---|
refreshCompleted() |
Notify refresh is done |
refreshFailed() |
Notify refresh failed |
loadComplete() |
Notify load-more is done |
loadFailed() |
Notify load-more failed |
loadNoData() |
Notify no more data to load |
resetNoData() |
Reset no-data state |
- NestedScrollView: Quick scroll direction changes may cause bounce-back due to Flutter's
BouncingScrollPhysicshandling. Related Flutter issues: #34316, #33367, #29264. - AnimatedList / ReorderableListView: Not directly supported as
child. UseSliverAnimatedListinside aCustomScrollViewinstead.
- Inspired by flutter_pulltorefresh by peng8350
- SmartRefreshLayout (Android)
MIT License β see LICENSE for details.





















