下拉刷新和加载更多
主要方式:
- RefreshIndicator (Material 风格)
- CustomScrollView + SliverRefreshIndicator (sliver 版本)
- CupertinoSliverRefreshControl (iOS 风格)
- 第三方库:easy_refresh, pull_to_refresh, smart_refresher 等
- 自定义 RefreshIndicator (自定义 indicator builder)
- 直接使用 ScrollController + NotificationListener 手动实现
让我给一个清晰的对比和示例。 Flutter 下拉刷新主要有这几种方案,按推荐度排序:
1. RefreshIndicator(Material 风格,最常用)
scala
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<String> items = ['1', '2', '3'];
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 5));
setState(() {
// 先把"目标长度"算出来存局部变量,避免循环里读 items.length
// (否则 add 一次 length 就 +1,i 永远追不上,变成死循环)
final int start = items.length;
const int addCount = 3;
for (int i = start; i < start + addCount; i++) {
items.add((i + 1).toString());
}
});
},
child: CustomScrollView(
// 关键:始终允许滚动,否则内容不足一屏时下拉刷新不触发
physics: const AlwaysScrollableScrollPhysics(),
slivers: items
.map(
(e) => SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
alignment: Alignment.center,
color: Colors.amber,
child: Text(e),
),
),
)
.toList(),
),
);
}
}
- 优点:官方、稳定、API 简单
- 缺点:indicator 样式固定(圆形转圈),定制性弱
2. RefreshIndicator + 自定义 indicator(Flutter 3.x+)
less
RefreshIndicator(
onRefresh: () async => _loadData(),
refreshTriggerPullDistance: 80, // 触发距离
strokeWidth: 3, // 线条粗细
// 自定义颜色
backgroundColor: Colors.white,
color: Theme.of(context).colorScheme.primary,
child: ListView(...),
)
更彻底的自定义:
dart
RefreshIndicator(
onRefresh: _loadData,
// 用 notification 路由 + 自绘 indicator(通过 NotificationListener)
notificationPredicate: (notification) {
return notification is ScrollStartNotification;
},
child: ...,
)
3. CupertinoSliverRefreshControl(iOS 风格)
less
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<String> items = ['1', '2', '3'];
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
int begin = Random().nextInt(100);
print(begin);
int end = begin + 3;
List<String> newlist = [];
for (int i = begin; i < end; i++) {
newlist.add((i + 1).toString());
}
items = newlist;
});
},
),
...items
.map(
(e) => SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
color: Colors.amber,
alignment: Alignment.center,
child: Text(e),
),
),
)
.toList(),
],
);
}
}
- 优点:iOS 原生体验,弹性回弹
- 缺点:必须在
CustomScrollView里用(sliver 体系) - 这是 iOS 端最地道的方式,下拉时会有 iOS 风格的圆圈加载动画
自定义下拉框
less
import 'dart:math' as math;
import 'dart:math' show Random;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<String> items = ['1', '2', '3'];
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
// ============================================================
// CupertinoSliverRefreshControl 的 builder 用法
// ============================================================
// builder 签名:
// Widget Function(
// BuildContext context,
// RefreshIndicatorMode refreshState, // 当前状态
// double pulledExtent, // 已下拉距离
// double refreshTriggerPullDistance, // 触发阈值
// double refreshIndicatorExtent, // 刷新时固定高度
// )
//
// RefreshIndicatorMode 状态机:
// inactive - 未激活,sliver 高度 = 0
// drag - 用户正在下拉,pulledExtent < refreshTriggerPullDistance
// armed - 已拉到/超过阈值,松手就会触发刷新
// refresh - onRefresh 正在执行,sliver 高度固定为 refreshIndicatorExtent
// done - onRefresh 完成,正在收起
// ============================================================
CupertinoSliverRefreshControl(
// 触发刷新需要下拉的距离,默认 100
// refreshTriggerPullDistance: 100,
// 刷新时 indicator 占据的固定高度,默认 60
// refreshIndicatorExtent: 60,
builder: (context, refreshState, pulledExtent, refreshTriggerPullDistance, refreshIndicatorExtent) {
// 容器高度随下拉距离变化;刷新时固定为 refreshIndicatorExtent
final double height = pulledExtent.clamp(0.0, refreshIndicatorExtent);
// 根据状态切换不同 UI
Widget child;
switch (refreshState) {
case RefreshIndicatorMode.inactive:
// 未激活时返回空,避免占空间
child = const SizedBox.shrink();
break;
case RefreshIndicatorMode.drag:
// 下拉中:用进度展示"拉了多少"
final progress =
(pulledExtent / refreshTriggerPullDistance).clamp(0.0, 1.0);
child = Column(
mainAxisSize: MainAxisSize.min,
children: [
// 箭头随下拉距离旋转 0 → π(180°)
Transform.rotate(
angle: progress * math.pi,
child: const Icon(Icons.arrow_downward, size: 24),
),
const SizedBox(height: 4),
Text(
progress < 1.0 ? '下拉刷新' : '松开刷新',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
);
break;
case RefreshIndicatorMode.armed:
// 达到阈值,松手即触发
child = const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_upward, size: 24),
SizedBox(height: 4),
Text('松开刷新',
style: TextStyle(fontSize: 12, color: Colors.grey)),
],
);
break;
case RefreshIndicatorMode.refresh:
// 刷新中:显示菊花
child = const Column(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoActivityIndicator(radius: 12),
SizedBox(height: 4),
Text('刷新中...',
style: TextStyle(fontSize: 12, color: Colors.grey)),
],
);
break;
case RefreshIndicatorMode.done:
// 完成,正在收起
child = const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check, size: 24, color: Colors.green),
SizedBox(height: 4),
Text('刷新完成',
style: TextStyle(fontSize: 12, color: Colors.grey)),
],
);
break;
}
return Container(
height: height,
alignment: Alignment.center,
child: child,
);
},
onRefresh: () async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
int begin = Random().nextInt(100);
print(begin);
int end = begin + 3;
List<String> newlist = [];
for (int i = begin; i < end; i++) {
newlist.add((i + 1).toString());
}
items = newlist;
});
},
),
...items
.map(
(e) => SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
color: Colors.amber,
alignment: Alignment.center,
child: Text(e),
),
),
)
.toList(),
],
);
}
}
5. 第三方库(强烈推荐生产环境使用)
pull_to_refresh / smart_refresher(最流行)
yaml
dependencies:
pull_to_refresh: ^2.0.0
dart
import 'dart:math' show Random;
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
void main(List<String> args) {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<String> items = ['1', '2', '3'];
// SmartRefresher 的控制器,负责通知刷新/加载完成
final RefreshController _controller = RefreshController(initialRefresh: true);
@override
void initState() {
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// 下拉刷新:重置为第一页数据(模拟重新加载)
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
int begin = Random().nextInt(100);
List<String> newList = [];
for (int i = begin; i < begin + 10; i++) {
// 第一页 10 条
newList.add((i + 1).toString());
}
items = newList;
});
// 通知 SmartRefresher 刷新完成,indicator 自动收起
_controller.refreshCompleted();
}
// 上拉加载更多:追加数据
Future<void> _onLoading() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
final int start = items.length;
for (int i = start; i < start + 30; i++) {
items.add((i + 1).toString());
}
});
// 通知 SmartRefresher 加载完成,footer 自动收起
_controller.loadComplete();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SmartRefresher 示例')),
body: SmartRefresher(
controller: _controller,
enablePullDown: true, // 开启下拉刷新
enablePullUp: true, // 开启上拉加载更多
header: const WaterDropHeader(), // 下拉刷新样式(水滴)
footer: const ClassicFooter(), // 上拉加载样式(经典)
onRefresh: _onRefresh,
onLoading: _onLoading,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
),
),
);
}
}
- 优点:
- 同时支持下拉刷新 + 上拉加载更多(RefreshIndicator 不支持上拉)
- 内置 10+ 种 header/footer 样式(经典、水滴、Bezier、Material、Bezier 等)
- 高度可定制,可自定义 indicator widget
- 缺点:需要管理 controller 状态
easy_refresh
yaml
dependencies:
easy_refresh: ^3.4.0
dart
import 'dart:math' show Random;
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<String> items = ['1', '2', '3'];
// EasyRefresh 的控制器
// - controlFinishRefresh: true 时需要手动调 finishRefresh(),可以传 IndicatorResult
// - controlFinishRefresh: false(默认)时 onRefresh 返回 Future 完成后自动结束
// 这里用默认(自动结束),更简洁
final EasyRefreshController _controller = EasyRefreshController(
controlFinishRefresh: false,
controlFinishLoad: false,
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// 下拉刷新:重置为第一页数据(模拟重新加载)
// EasyRefresh 的 onRefresh 返回 Future,Future 完成后自动收起 indicator
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
int begin = Random().nextInt(100);
List<String> newList = [];
for (int i = begin; i < begin + 10; i++) {
newList.add((i + 1).toString());
}
items = newList;
});
// 默认模式(controlFinishRefresh: false)下无需手动调用 finishRefresh
// 如果想根据结果区分(成功/失败/无更多),可以:
// _controller.finishRefresh(IndicatorResult.success);
// _controller.finishRefresh(IndicatorResult.fail);
// _controller.finishRefresh(IndicatorResult.noMore);
}
// 上拉加载更多:追加数据
Future<void> _onLoad() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
final int start = items.length;
for (int i = start; i < start + 30; i++) {
items.add((i + 1).toString());
}
});
// 默认模式下无需手动 finishLoad
// 没有更多数据时:_controller.finishLoad(IndicatorResult.noMore);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('EasyRefresh 示例')),
body: EasyRefresh(
controller: _controller,
// 下拉刷新样式:Material 风格的圆形进度条
header: const MaterialHeader(),
// 上拉加载样式:经典样式
footer: const ClassicFooter(),
onRefresh: _onRefresh,
onLoad: _onLoad,
// refreshOnStart: true, // 进入页面自动触发一次刷新
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
),
),
);
}
}
- 比
smart_refresher更现代化,API 更简洁 - 内置 Material、Bezier、Custom 等多种样式
6. 完全自定义(手写)
适合需要非常特殊效果(如 Lottie 动画、SVG 拉伸等):
dart
class CustomRefresh extends StatefulWidget {
...
}
class _CustomRefreshState extends State<CustomRefresh> {
final ScrollController _controller = ScrollController();
double _dragOffset = 0;
bool _isRefreshing = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
// 监听 overscroll
if (_controller.position.outOfRange()) {
setState(() => _dragOffset = -_controller.position.pixels);
}
});
}
Future<void> _handleRefresh() async {
setState(() => _isRefreshing = true);
await widget.onRefresh();
setState(() => _isRefreshing = false);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// 自定义 header(如 Lottie 动画)
Positioned(
top: _dragOffset - 60,
child: SizedBox(
height: 60,
child: _isRefreshing
? Lottie.asset('animations/loading.json')
: const CircularProgressIndicator(),
),
),
ListView.builder(
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
...
),
],
);
}
}
适合复杂动画场景,但实现成本高。
对比表
| 方案 | 下拉 | 上拉 | 样式定制 | 平台体验 | 推荐度 |
|---|---|---|---|---|---|
RefreshIndicator |
✅ | ❌ | 弱 | Material | ⭐⭐⭐ |
CupertinoSliverRefreshControl |
✅ | ❌ | 弱 | iOS 原生 | ⭐⭐⭐ |
SliverRefreshIndicator |
✅ | ❌ | 弱 | Material | ⭐⭐ |
smart_refresher |
✅ | ✅ | 强 | 跨平台 | ⭐⭐⭐⭐⭐ |
easy_refresh |
✅ | ✅ | 强 | 跨平台 | ⭐⭐⭐⭐⭐ |
| 完全自定义 | ✅ | ✅ | 完全自由 | 自定义 | ⭐⭐ |
实际选型建议
- 简单列表,只需要下拉刷新 →
RefreshIndicator- 代码量最少,没有额外依赖
- iOS 体验优先 →
CupertinoSliverRefreshControl- 注意要在
CustomScrollView里用
- 注意要在
- 需要上拉加载更多 / 想要好看的刷新动画 →
easy_refresh或smart_refresher- 生产环境最常用
- 需要 Lottie/自定义动画 → 完全自定义或
easy_refresh.builder(...)
关键代码片段:RefreshIndicator 配合 CarouselView
如果要给项目的 CustomScrollView 加下拉刷新(注意 sliver 体系下要用 RefreshIndicator 包整个 ScrollView,或者用 sliver 版):
dart
RefreshIndicator(
onRefresh: () async {
// 模拟数据刷新
await Future.delayed(const Duration(seconds: 1));
setState(() {/* 更新数据 */});
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), // 关键:保证滚动到顶也能继续下拉
slivers: [...],
),
)
重要 :
CustomScrollView必须加physics: AlwaysScrollableScrollPhysics(),否则当内容不足一屏时下拉刷新不灵敏。
一句话总结
简单场景用 RefreshIndicator,iOS 风格用 CupertinoSliverRefreshControl,生产环境需要"上拉加载 + 美观动画"时直接上 easy_refresh 或 smart_refresher。