Flutter (二十一) 下拉刷新,加载更多

下拉刷新和加载更多

主要方式:

  1. RefreshIndicator (Material 风格)
  2. CustomScrollView + SliverRefreshIndicator (sliver 版本)
  3. CupertinoSliverRefreshControl (iOS 风格)
  4. 第三方库:easy_refresh, pull_to_refresh, smart_refresher 等
  5. 自定义 RefreshIndicator (自定义 indicator builder)
  6. 直接使用 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 跨平台 ⭐⭐⭐⭐⭐
完全自定义 完全自由 自定义 ⭐⭐

实际选型建议

  1. 简单列表,只需要下拉刷新RefreshIndicator
    • 代码量最少,没有额外依赖
  2. iOS 体验优先CupertinoSliverRefreshControl
    • 注意要在 CustomScrollView 里用
  3. 需要上拉加载更多 / 想要好看的刷新动画easy_refreshsmart_refresher
    • 生产环境最常用
  4. 需要 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_refreshsmart_refresher

相关推荐
IT_陈寒3 小时前
Python装饰器把我坑惨了,原来这样用才不掉链子
前端·人工智能·后端
程序员爱钓鱼3 小时前
Rust Associated Type关联类型详解:为Trait定义内部类型
前端·后端·rust
zh_xuan3 小时前
个人主页左侧菜单支持分组,以及菜单显示和隐藏
前端·javascript·css
Emily156853598707 小时前
Emerson 1C31129G03 Analog Input Module
java·开发语言·前端·plc·emerson·1c31129g03·input module
To_OC12 小时前
面试被问了三回三栏布局,这次我终于把 BFC 那层窗户纸捅破了
前端·css·面试
To_OC12 小时前
啃完 TS 工具类型我发现:Pick 和 Omit 原来就是一层窗户纸
前端·面试·typescript
风月说与山鬼13 小时前
三、大括号语法
前端·react.js
kyriewen14 小时前
我把最常踩的8个CORS跨域报错整理了一遍——第8个去年还不存在
前端·javascript