Flutter实现瀑布流布局及下拉刷新上拉加载更多

393 阅读6分钟

在 Flutter 应用开发中,瀑布流布局常用于展示图片、商品列表等需要以不规则但整齐排列的内容。同时,下拉刷新和上拉加载更多功能,能够极大提升用户体验,让用户方便地获取最新和更多的数据。

1. 前置条件

  1. Flutter 环境已搭建(要求 Flutter 3.0+、Dart 2.17+)

  2. 本地图片资源已放入 assets/images 目录,并在 pubspec.yaml 中配置

  3. 已安装依赖插件并执行 flutter pub get

2. 结构分析

6210b7bafa9244369d7ce1aa5ca874be.gif

2.1 安装依赖插件

在项目的 pubspec.yaml 文件中添加以下依赖:


# 瀑布流布局:https://pub.dev/packages/waterfall_flow
waterfall_flow: ^3.0.3
# 上拉加载更多+下拉刷新:https://pub.dev/packages/pull_to_refresh
pull_to_refresh: ^2.0.0

注意:waterfall_flow: ^3.0.3 需适配 Flutter 3.0+,pull_to_refresh: ^2.0.0 需 Dart 2.17+

添加依赖后执行命令安装:


flutter pub get

2.2 配置本地图片资源

pubspec.yaml 中配置图片资源路径,确保 Flutter 能识别本地图片:


flutter:
  uses-material-design: true
  # 配置图片资源目录
  assets:
    - assets/images/

2.3 引入必要的库


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
  • dart:async:提供异步操作能力,用于处理刷新和加载更多的延迟模拟。

  • package:flutter/material.dart:Flutter 核心 UI 库,提供 Scaffold、Container 等基础组件。

  • package:pull_to_refresh/pull_to_refresh.dart:实现下拉刷新和上拉加载更多的核心库。

  • package:waterfall_flow/waterfall_flow.dart:用于快速构建瀑布流布局的第三方库。

2.4 定义图片枚举及扩展

为了规范图片资源管理,我们定义图片枚举,并通过扩展方法获取图片路径:


/// 本地图片枚举
enum ImageEnum {
  banner1,
  banner2,
  banner3,
  model1,
  model2,
  model3,
  model4,
}

/// 为枚举扩展获取图片路径的方法
extension ImageEnumExtension on ImageEnum {
  String get path {
    switch (this) {
      case ImageEnum.banner1:
        return 'assets/images/banner1.png';
      case ImageEnum.banner2:
        return 'assets/images/banner2.png';
      case ImageEnum.banner3:
        return 'assets/images/banner3.png';
      case ImageEnum.model1:
        return 'assets/images/model1.png';
      case ImageEnum.model2:
        return 'assets/images/model2.png';
      case ImageEnum.model3:
        return 'assets/images/model3.png';
      case ImageEnum.model4:
        return 'assets/images/model4.png';
    }
  }
}

2.5 定义 ImageWaterfallFlow 组件

定义有状态组件,用于管理瀑布流布局的状态和 UI 渲染:


class ImageWaterfallFlow extends StatefulWidget {
  const ImageWaterfallFlow({super.key});

  @override
  State<ImageWaterfallFlow> createState() => ImageWaterfallFlowState();
}

有状态组件可以根据用户操作或数据变化动态更新 UI,createState 方法返回状态管理类 ImageWaterfallFlowState

2.6 ImageWaterfallFlowState 类的详细解析

该类是组件的核心,负责管理数据、控制器和业务逻辑:


class ImageWaterfallFlowState extends State<ImageWaterfallFlow> {
  /// 字体样式
  final TextStyle myTxtStyle = const TextStyle(
      color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800);

  /// 模拟数据(初始数据)- 增加泛型提升类型安全
  List<ImageEnum> imageList = [
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4,
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4
  ];

  /// 模拟数据(加载更多使用)
  List<ImageEnum> moreList = [ImageEnum.banner1, ImageEnum.banner2, ImageEnum.banner3];

  /// 上拉下拉控制器
  final RefreshController myRefreshController = RefreshController();
  • myTxtStyle:定义图片上显示序号的字体样式。

  • imageList:存储瀑布流初始展示的图片数据,使用泛型 List<ImageEnum> 保证类型安全。

  • moreList:存储加载更多时需要追加的数据。

  • myRefreshController:来自 pull_to_refresh 库,用于控制刷新和加载的状态。

2.7 刷新和加载更多的方法

实现下拉刷新和上拉加载更多的业务逻辑,补充边界条件处理:


/// 刷新
void onRefresh() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  // 模拟刷新:恢复初始数据
  setState(() {
    imageList = [
      ImageEnum.banner1,
      ImageEnum.banner2,
      ImageEnum.banner3,
      ImageEnum.model1,
      ImageEnum.model2,
      ImageEnum.model3,
      ImageEnum.model4,
      ImageEnum.banner1,
      ImageEnum.banner2,
      ImageEnum.banner3,
      ImageEnum.model1,
      ImageEnum.model2,
      ImageEnum.model3,
      ImageEnum.model4
    ];
  });
  myRefreshController.refreshCompleted();
  myRefreshController.resetNoData(); // 重置无更多数据状态
}

/// 加载更多
void onLoadMore() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  // 模拟:数据超过30条时标记无更多数据
  if (imageList.length >= 30) {
    myRefreshController.loadNoData();
  } else {
    imageList.addAll(moreList);
    if (mounted) {
      setState(() {});
    }
    myRefreshController.loadComplete();
  }
}
  • onRefresh:下拉刷新时触发,模拟 1 秒延迟后恢复初始数据,并重置加载状态。

  • onLoadMore:上拉加载时触发,模拟 1 秒延迟后追加数据;当数据量超过 30 条时,标记为“无更多数据”。

  • mounted 判断:防止组件销毁后调用 setState 导致异常。

2.8 资源释放

重写 dispose 方法,释放控制器资源,避免内存泄漏:


@override
void dispose() {
  myRefreshController.dispose(); // 释放刷新控制器
  super.dispose();
}

2.9 构建 UI 的方法

构建组件的基础布局结构:


/// 布局
@override
Widget build(BuildContext context) {
  return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
          child: SizedBox(
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              child: listWidget())));
}
  • Scaffold:作为页面的根布局,设置背景色为黑色。

  • SafeArea:避免内容被刘海屏、状态栏等遮挡。

  • SizedBox:设置与屏幕同宽同高的容器,承载瀑布流列表。

2.10 构建瀑布流列表的方法

整合刷新组件和瀑布流布局,实现核心 UI 效果:


/// 列表
Widget listWidget() {
  return SmartRefresher(
    enablePullDown: true,
    enablePullUp: true,
    header: const ClassicHeader(),
    footer: const ClassicFooter(),
    controller: myRefreshController,
    onRefresh: onRefresh,
    onLoading: onLoadMore,
    child: WaterfallFlow.builder(
      padding: const EdgeInsets.all(10), // 增加内边距,避免内容贴边
      physics: const BouncingScrollPhysics(),
      gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 20,
        mainAxisSpacing: 20,
        viewportBuilder: (int index1, int index2) {
          print('变化:$index1-$index2');
        },
        // 修正最后一个子项的判断逻辑(索引从0开始)
        lastChildLayoutTypeBuilder: (index) => index == imageList.length - 1
            ? LastChildLayoutType.fullCrossAxisExtent
            : LastChildLayoutType.none,
      ),
      itemCount: imageList.length,
      itemBuilder: (BuildContext context, int index) {
        return Container(
          color: Colors.white,
          height: (index + 1) % 2 == 0 ? 100 : 200,
          child: Container(
              alignment: Alignment.center,
              decoration: BoxDecoration(
                  color: Colors.blue.shade300,
                  image: DecorationImage(
                    image: AssetImage(imageList[index].path), // 调用扩展方法获取图片路径
                    fit: BoxFit.cover,
                  )),
              child: Text('第${index + 1}张', style: myTxtStyle)),
        );
      },
    ),
  );
}
  • SmartRefresherpull_to_refresh 库的核心组件,配置上下拉开关、头部底部样式和回调方法。

  • WaterfallFlow.builder:瀑布流布局的构建器,通过懒加载方式渲染子项:

    • padding:增加内边距,优化视觉效果。

    • crossAxisCount: 2:设置瀑布流为 2 列布局。

    • crossAxisSpacing/mainAxisSpacing:设置子项之间的水平和垂直间距。

    • lastChildLayoutTypeBuilder:修正索引判断逻辑,让最后一个子项占满整行宽度。

    • itemBuilder:构建每个瀑布流子项,通过 AssetImage(imageList[index].path) 获取图片路径,设置不同高度实现瀑布流效果。

3. 核心库 API & 属性说明

3.1 waterfall_flow 核心 API/属性

3.1.1 WaterfallFlow 核心组件

属性名类型说明默认值
paddingEdgeInsetsGeometry瀑布流整体内边距EdgeInsets.zero
physicsScrollPhysics滚动物理效果AlwaysScrollableScrollPhysics()
shrinkWrapbool是否根据子项高度自适应false
gridDelegateSliverWaterfallFlowDelegate瀑布流布局代理(核心)无(必传)
childrenList<Widget>子组件列表(非懒加载)[]
itemCountint子项数量(builder 模式)无(builder 模式必传)
itemBuilderIndexedWidgetBuilder子项构建器(懒加载)无(builder 模式必传)

3.1.2 SliverWaterfallFlowDelegateWithFixedCrossAxisCount(常用布局代理)

属性名类型说明默认值
crossAxisCountint列数(核心)无(必传)
crossAxisSpacingdouble列之间的间距0.0
mainAxisSpacingdouble行之间的间距0.0
lastChildLayoutTypeBuilderLastChildLayoutTypeBuilder最后一个子项布局类型null
viewportBuilderViewportBuilder视口内子项变化回调null
collectGarbageCollectGarbage回收不可见子项时的回调null

3.1.3 LastChildLayoutType(最后一个子项布局类型)

枚举值说明
LastChildLayoutType.none无特殊布局(默认)
LastChildLayoutType.fullCrossAxisExtent占满整行宽度
LastChildLayoutType.footnote脚注样式(小尺寸)

3.2 pull_to_refresh 核心 API/属性

3.2.1 SmartRefresher(核心组件)

属性名类型说明默认值
enablePullDownbool是否启用下拉刷新true
enablePullUpbool是否启用上拉加载false
headerRefreshHeader下拉刷新头部样式ClassicHeader()
footerLoadFooter上拉加载底部样式ClassicFooter()
controllerRefreshController刷新/加载控制器(核心)无(必传)
onRefreshVoidCallback?下拉刷新回调null
onLoadingVoidCallback?上拉加载回调null
childWidget包裹的子组件(如列表/瀑布流)无(必传)
scrollDirectionAxis滚动方向Axis.vertical
physicsScrollPhysics滚动物理效果ClampingScrollPhysics()
enableTwoLevelbool是否启用二级刷新(如下拉展开更多)false

3.2.2 RefreshController(核心控制器)

方法名说明
refreshCompleted()标记下拉刷新完成
refreshFailed()标记下拉刷新失败
loadComplete()标记上拉加载完成
loadNoData()标记无更多数据
resetNoData()重置无更多数据状态
dispose()释放控制器资源(必写,避免内存泄漏)
requestRefresh()主动触发下拉刷新
requestLoading()主动触发上拉加载

3.2.3 ClassicHeader/ClassicFooter(经典样式)

属性名类型说明默认值
heightdouble头部/底部高度60.0
idleTextString闲置状态文本下拉刷新/上拉加载
refreshingTextString刷新中/加载中文本刷新中/加载中
completeTextString完成状态文本刷新完成/加载完成
failedTextString失败状态文本刷新失败/加载失败
noDataTextString无更多数据文本暂无更多数据
textStyleTextStyle文本样式灰色 14 号字
iconSizedouble图标大小20.0

4. 完整代码


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:waterfall_flow/waterfall_flow.dart';

/// 本地图片枚举
enum ImageEnum {
  banner1,
  banner2,
  banner3,
  model1,
  model2,
  model3,
  model4,
}

/// 为枚举扩展获取图片路径的方法
extension ImageEnumExtension on ImageEnum {
  String get path {
    switch (this) {
      case ImageEnum.banner1:
        return 'assets/images/banner1.png';
      case ImageEnum.banner2:
        return 'assets/images/banner2.png';
      case ImageEnum.banner3:
        return 'assets/images/banner3.png';
      case ImageEnum.model1:
        return 'assets/images/model1.png';
      case ImageEnum.model2:
        return 'assets/images/model2.png';
      case ImageEnum.model3:
        return 'assets/images/model3.png';
      case ImageEnum.model4:
        return 'assets/images/model4.png';
    }
  }
}

/// 瀑布流组件
class ImageWaterfallFlow extends StatefulWidget {
  const ImageWaterfallFlow({super.key});

  @override
  State<ImageWaterfallFlow> createState() => ImageWaterfallFlowState();
}

class ImageWaterfallFlowState extends State<ImageWaterfallFlow> {
  /// 字体样式
  final TextStyle myTxtStyle = const TextStyle(
      color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800);

  /// 模拟数据(初始数据)- 增加泛型提升类型安全
  List<ImageEnum> imageList = [
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4,
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4
  ];

  /// 模拟数据(加载更多使用)
  List<ImageEnum> moreList = [ImageEnum.banner1, ImageEnum.banner2, ImageEnum.banner3];

  /// 上拉下拉控制器
  final RefreshController myRefreshController = RefreshController();

  /// 刷新
  void onRefresh() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    // 模拟刷新:恢复初始数据
    setState(() {
      imageList = [
        ImageEnum.banner1,
        ImageEnum.banner2,
        ImageEnum.banner3,
        ImageEnum.model1,
        ImageEnum.model2,
        ImageEnum.model3,
        ImageEnum.model4,
        ImageEnum.banner1,
        ImageEnum.banner2,
        ImageEnum.banner3,
        ImageEnum.model1,
        ImageEnum.model2,
        ImageEnum.model3,
        ImageEnum.model4
      ];
    });
    myRefreshController.refreshCompleted();
    myRefreshController.resetNoData(); // 重置无更多数据状态
  }

  /// 加载更多
  void onLoadMore() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    // 模拟:数据超过30条时标记无更多数据
    if (imageList.length >= 30) {
      myRefreshController.loadNoData();
    } else {
      imageList.addAll(moreList);
      if (mounted) {
        setState(() {});
      }
      myRefreshController.loadComplete();
    }
  }

  @override
  void dispose() {
    myRefreshController.dispose(); // 释放控制器资源
    super.dispose();
  }

  /// 布局
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.black,
        body: SafeArea(
            child: SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                child: listWidget())));
  }

  /// 列表
  Widget listWidget() {
    return SmartRefresher(
      enablePullDown: true,
      enablePullUp: true,
      header: const ClassicHeader(),
      footer: const ClassicFooter(),
      controller: myRefreshController,
      onRefresh: onRefresh,
      onLoading: onLoadMore,
      child: WaterfallFlow.builder(
        padding: const EdgeInsets.all(10),
        physics: const BouncingScrollPhysics(),
        gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
          viewportBuilder: (int index1, int index2) {
            print('变化:$index1-$index2');
          },
          // 修正最后一个子项的判断逻辑
          lastChildLayoutTypeBuilder: (index) => index == imageList.length - 1
              ? LastChildLayoutType.fullCrossAxisExtent
              : LastChildLayoutType.none,
        ),
        itemCount: imageList.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            color: Colors.white,
            height: (index + 1) % 2 == 0 ? 100 : 200,
            child: Container(
                alignment: Alignment.center,
                decoration: BoxDecoration(
                    color: Colors.blue.shade300,
                    image: DecorationImage(
                      image: AssetImage(imageList[index].path),
                      fit: BoxFit.cover,
                    )),
                child: Text('第${index + 1}张', style: myTxtStyle)),
          );
        },
      ),
    );
  }
}

5. 常见问题 & 解决方案

  1. 图片不显示

    • 检查 pubspec.yamlassets 配置路径是否与实际图片存放路径一致。

    • 执行 flutter clean 清理缓存后,重新运行项目。

    • 确认枚举扩展方法 path 返回的路径正确。

  2. 加载更多无响应

    • 确认 SmartRefresherenablePullUp 属性设置为 true

    • 检查 RefreshController 是否正确关联到 SmartRefresher

    • 确保 onLoading 回调方法已正确绑定。

  3. 瀑布流布局错乱

    • 避免子项高度差异过大,可根据实际需求调整高度规则。

    • 检查 crossAxisCountcrossAxisSpacing 等参数配置是否冲突。

    • 确认 lastChildLayoutTypeBuilder 的索引判断逻辑正确。

  4. 内存泄漏

    • 务必在 dispose 方法中调用 myRefreshController.dispose() 释放控制器。

    • 避免在异步操作中未判断 mounted 就调用 setState

6. 总结

通过 waterfall_flowpull_to_refresh 两个第三方库,我们快速实现了 Flutter 瀑布流布局,并集成了下拉刷新和上拉加载更多功能。

该方案可直接应用于图片列表、商品展示等常见业务场景,也可根据实际需求扩展子项点击、图片懒加载、自定义刷新样式等功能。

希望这篇文章能帮助你理解并在自己的 Flutter 项目中运用类似的功能。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章