在 Flutter 应用开发中,瀑布流布局常用于展示图片、商品列表等需要以不规则但整齐排列的内容。同时,下拉刷新和上拉加载更多功能,能够极大提升用户体验,让用户方便地获取最新和更多的数据。
1. 前置条件
-
Flutter 环境已搭建(要求 Flutter 3.0+、Dart 2.17+)
-
本地图片资源已放入
assets/images目录,并在pubspec.yaml中配置 -
已安装依赖插件并执行
flutter pub get
2. 结构分析
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)),
);
},
),
);
}
-
SmartRefresher:pull_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 核心组件
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
padding | EdgeInsetsGeometry | 瀑布流整体内边距 | EdgeInsets.zero |
physics | ScrollPhysics | 滚动物理效果 | AlwaysScrollableScrollPhysics() |
shrinkWrap | bool | 是否根据子项高度自适应 | false |
gridDelegate | SliverWaterfallFlowDelegate | 瀑布流布局代理(核心) | 无(必传) |
children | List<Widget> | 子组件列表(非懒加载) | [] |
itemCount | int | 子项数量(builder 模式) | 无(builder 模式必传) |
itemBuilder | IndexedWidgetBuilder | 子项构建器(懒加载) | 无(builder 模式必传) |
3.1.2 SliverWaterfallFlowDelegateWithFixedCrossAxisCount(常用布局代理)
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
crossAxisCount | int | 列数(核心) | 无(必传) |
crossAxisSpacing | double | 列之间的间距 | 0.0 |
mainAxisSpacing | double | 行之间的间距 | 0.0 |
lastChildLayoutTypeBuilder | LastChildLayoutTypeBuilder | 最后一个子项布局类型 | null |
viewportBuilder | ViewportBuilder | 视口内子项变化回调 | null |
collectGarbage | CollectGarbage | 回收不可见子项时的回调 | null |
3.1.3 LastChildLayoutType(最后一个子项布局类型)
| 枚举值 | 说明 |
|---|---|
LastChildLayoutType.none | 无特殊布局(默认) |
LastChildLayoutType.fullCrossAxisExtent | 占满整行宽度 |
LastChildLayoutType.footnote | 脚注样式(小尺寸) |
3.2 pull_to_refresh 核心 API/属性
3.2.1 SmartRefresher(核心组件)
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
enablePullDown | bool | 是否启用下拉刷新 | true |
enablePullUp | bool | 是否启用上拉加载 | false |
header | RefreshHeader | 下拉刷新头部样式 | ClassicHeader() |
footer | LoadFooter | 上拉加载底部样式 | ClassicFooter() |
controller | RefreshController | 刷新/加载控制器(核心) | 无(必传) |
onRefresh | VoidCallback? | 下拉刷新回调 | null |
onLoading | VoidCallback? | 上拉加载回调 | null |
child | Widget | 包裹的子组件(如列表/瀑布流) | 无(必传) |
scrollDirection | Axis | 滚动方向 | Axis.vertical |
physics | ScrollPhysics | 滚动物理效果 | ClampingScrollPhysics() |
enableTwoLevel | bool | 是否启用二级刷新(如下拉展开更多) | false |
3.2.2 RefreshController(核心控制器)
| 方法名 | 说明 |
|---|---|
refreshCompleted() | 标记下拉刷新完成 |
refreshFailed() | 标记下拉刷新失败 |
loadComplete() | 标记上拉加载完成 |
loadNoData() | 标记无更多数据 |
resetNoData() | 重置无更多数据状态 |
dispose() | 释放控制器资源(必写,避免内存泄漏) |
requestRefresh() | 主动触发下拉刷新 |
requestLoading() | 主动触发上拉加载 |
3.2.3 ClassicHeader/ClassicFooter(经典样式)
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
height | double | 头部/底部高度 | 60.0 |
idleText | String | 闲置状态文本 | 下拉刷新/上拉加载 |
refreshingText | String | 刷新中/加载中文本 | 刷新中/加载中 |
completeText | String | 完成状态文本 | 刷新完成/加载完成 |
failedText | String | 失败状态文本 | 刷新失败/加载失败 |
noDataText | String | 无更多数据文本 | 暂无更多数据 |
textStyle | TextStyle | 文本样式 | 灰色 14 号字 |
iconSize | double | 图标大小 | 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. 常见问题 & 解决方案
-
图片不显示
-
检查
pubspec.yaml中assets配置路径是否与实际图片存放路径一致。 -
执行
flutter clean清理缓存后,重新运行项目。 -
确认枚举扩展方法
path返回的路径正确。
-
-
加载更多无响应
-
确认
SmartRefresher的enablePullUp属性设置为true。 -
检查
RefreshController是否正确关联到SmartRefresher。 -
确保
onLoading回调方法已正确绑定。
-
-
瀑布流布局错乱
-
避免子项高度差异过大,可根据实际需求调整高度规则。
-
检查
crossAxisCount、crossAxisSpacing等参数配置是否冲突。 -
确认
lastChildLayoutTypeBuilder的索引判断逻辑正确。
-
-
内存泄漏
-
务必在
dispose方法中调用myRefreshController.dispose()释放控制器。 -
避免在异步操作中未判断
mounted就调用setState。
-
6. 总结
通过 waterfall_flow 和 pull_to_refresh 两个第三方库,我们快速实现了 Flutter 瀑布流布局,并集成了下拉刷新和上拉加载更多功能。
该方案可直接应用于图片列表、商品展示等常见业务场景,也可根据实际需求扩展子项点击、图片懒加载、自定义刷新样式等功能。
希望这篇文章能帮助你理解并在自己的 Flutter 项目中运用类似的功能。
本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~
PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~
往期文章
- flutter使用package_info_plus库获取应用信息的教程
- Flutter下拉刷新上拉加载侧拉刷新插件:easy_refresh全面使用指南
- flutter-使用EventBus实现组件间数据通信
- Flutter输入框TextField的属性与实战用法全面解析+示例
- Flutter自定义日历table_calendar完全指南+案例
- flutter-屏幕自适应插件flutter_screenutil教程全指南
- flutter-使用url_launcher打开链接/应用/短信/邮件和评分跳转等
- flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
- 解锁flutter弹窗新姿势:dialog-flutter_smart_dialog插件解读+案例
- flutter-切换状态显示不同组件10种实现方案全解析
- flutter-详解控制组件显示的两种方式Offstage与Visibility
- flutter-使用AnimatedDefaultTextStyle实现文本动画
- flutter-使用SafeArea组件处理各机型的安全距离
- flutter-实现渐变色边框背景以及渐变色文字
- flutter-使用confetti制作炫酷纸屑爆炸粒子动画