前言:
本讲主要聚焦在懒加载和滚动列表上,如何用 Flutter 优雅、高效地展示海量数据,并实现符合用户习惯的滚动交互,比较有用。
一、总览
-
懒加载核心:ListView.builder/separated、GridView.builder 基于 Sliver 实现按需渲染,仅构建可视区域组件,解决大量数据渲染性能问题;
-
滚动控制:ScrollController 用于监听滚动位置、控制滚动行为,需在 dispose 中销毁避免内存泄漏;
-
交互扩展:RefreshIndicator 实现下拉刷新,结合 ScrollController 监听实现上拉加载更多,需加加载锁避免重复请求。
本章节聚焦于 Flutter 中长列表/网格数据的高效展示和交互优化,核心解决以下问题:
- 大量数据渲染时的性能问题(懒加载)
- 列表/网格的多样化布局(线性/网格)
- 滚动行为的监听与控制(滚动位置、滚动状态)
- 移动端常见的下拉刷新、上拉加载更多交互实现
- 懒加载本质:ListView.builder/GridView 基于 Sliver 实现,只构建可视区域内的组件,出界组件会被销毁,大幅降低内存占用;
- 滚动体系:Scrollable 处理滚动输入 → Viewport 限定可视区域 → Sliver 负责具体内容渲染;
- 交互扩展:基于 ScrollController/ScrollNotification 实现滚动监听,结合 RefreshIndicator 实现下拉刷新,上拉加载更多。
二、核心技术详解
1. ListView:列表渲染
(1)ListView.builder(基础懒加载列表)
核心属性
| 属性 | 作用 | 必选 |
|---|---|---|
| itemCount | 列表总条数 | 否(无限列表可设为null) |
| itemBuilder | 列表项构建函数(index → Widget) | 是 |
| scrollDirection | 滚动方向(Axis.vertical/horizontal) | 否(默认垂直) |
| physics | 滚动物理效果(BouncingScrollPhysics等) | 否 |
| controller | 滚动控制器 | 否 |
案例代码
import 'package:flutter/material.dart';
class ListViewBuilderDemo extends StatelessWidget {
// 模拟数据源
final List<String> dataList = List.generate(1000, (index) => "列表项 ${index + 1}");
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView.builder 懒加载")),
body: ListView.builder(
// 列表项数量
itemCount: dataList.length,
// 懒加载构建列表项
itemBuilder: (context, index) {
// 打印日志验证懒加载(只有可视区域会打印)
print("构建列表项:$index");
return ListTile(
title: Text(dataList[index]),
leading: Icon(Icons.list),
);
},
),
);
}
}
注意事项
- itemCount 设为 null 时为无限列表,需配合上拉加载更多使用;
- itemBuilder 中避免复杂计算,否则会导致滚动卡顿;
- 列表项高度不固定时,Flutter 会自动计算,若高度固定可通过
itemExtent属性指定,提升性能。
(2)ListView.separated(带分隔线的懒加载列表)
核心属性
在 ListView.builder 基础上新增:
| 属性 | 作用 | 必选 |
|---|---|---|
| separatorBuilder | 分隔线构建函数 | 是 |
案例代码
class ListViewSeparatedDemo extends StatelessWidget {
final List<String> dataList = List.generate(50, (index) => "带分隔线列表项 ${index + 1}");
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView.separated 带分隔线")),
body: ListView.separated(
itemCount: dataList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(dataList[index]),
leading: Icon(Icons.list_alt),
);
},
// 构建分隔线
separatorBuilder: (context, index) {
return Divider(
color: Colors.grey[300],
height: 1,
indent: 16, // 左侧缩进
endIndent: 16, // 右侧缩进
);
},
),
);
}
}
注意事项
- separatorBuilder 不会在最后一项后生成分隔线;
- 分隔线高度通过 Divider 的 height 属性控制,避免设置过大导致空间浪费。
2. GridView:网格列表
核心属性
| 属性 | 作用 | 必选 |
|---|---|---|
| gridDelegate | 网格布局委托(控制列数/宽高比) | 是 |
| itemCount | 网格项数量 | 否 |
| itemBuilder | 网格项构建函数 | 是 |
| physics | 滚动物理效果 | 否 |
| controller | 滚动控制器 | 否 |
常用网格委托
SliverGridDelegateWithFixedCrossAxisCount:固定列数SliverGridDelegateWithMaxCrossAxisExtent:固定子项最大宽度
案例代码(固定列数)
class GridViewDemo extends StatelessWidget {
final List<String> gridData = List.generate(30, (index) => "网格项 ${index + 1}");
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("GridView 网格列表")),
body: GridView.builder(
itemCount: gridData.length,
// 网格布局配置:2列,间距10
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 列数
crossAxisSpacing: 10, // 列间距
mainAxisSpacing: 10, // 行间距
childAspectRatio: 1.5, // 子项宽高比(宽/高)
),
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
color: Colors.blue[100 + index % 9 * 100],
child: Text(gridData[index]),
);
},
),
);
}
}
注意事项
- childAspectRatio 是宽/高比值,需根据设计稿调整;
- 网格项数量过多时,同样支持懒加载,原理与 ListView 一致;
- 避免网格项内嵌套滚动组件,会导致滚动冲突。
3. ScrollController:滚动监听与控制
核心功能
- 监听滚动位置、滚动状态;
- 控制滚动到指定位置(跳转到顶部/底部、指定索引);
- 监听滚动结束/开始事件。
案例代码
class ScrollControllerDemo extends StatefulWidget {
@override
_ScrollControllerDemoState createState() => _ScrollControllerDemoState();
}
class _ScrollControllerDemoState extends State<ScrollControllerDemo> {
late ScrollController _scrollController;
// 滚动位置
double _scrollOffset = 0.0;
// 是否到达底部
bool _isAtBottom = false;
@override
void initState() {
super.initState();
// 初始化控制器
_scrollController = ScrollController();
// 监听滚动事件
_scrollController.addListener(() {
setState(() {
_scrollOffset = _scrollController.offset;
// 判断是否到达底部(偏移量 + 可视高度 >= 总高度)
_isAtBottom = _scrollController.offset + _scrollController.position.viewportDimension
>= _scrollController.position.maxScrollExtent - 10;
});
});
}
// 滚动到顶部
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: Duration(milliseconds: 500),
curve: Curves.ease,
);
}
@override
void dispose() {
// 销毁控制器,避免内存泄漏
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ScrollController 滚动控制"),
actions: [
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: _scrollToTop,
)
],
),
body: Column(
children: [
// 滚动状态显示
Padding(
padding: EdgeInsets.all(8),
child: Text(
"滚动偏移:$_scrollOffset\n是否到底部:$_isAtBottom",
style: TextStyle(fontSize: 16),
),
),
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("滚动列表项 $index"));
},
),
),
],
),
);
}
}
注意事项
- 必须在 dispose 中销毁 ScrollController,否则会导致内存泄漏;
- 避免在滚动监听中执行耗时操作,会导致滚动卡顿;
- 判断是否到底部时,建议预留 10px 误差(避免因精度问题判断不准)。
4. 下拉刷新 & 上拉加载更多
(1)下拉刷新:RefreshIndicator
核心属性
| 属性 | 作用 | 必选 |
|---|---|---|
| onRefresh | 刷新回调函数(返回Future) | 是 |
| child | 刷新的列表/网格组件 | 是 |
| color | 刷新指示器颜色 | 否 |
| displacement | 刷新指示器下拉的距离 | 否 |
(2)上拉加载更多:基于 ScrollNotification/ScrollController
案例代码(组合实现)
class RefreshLoadMoreDemo extends StatefulWidget {
const RefreshLoadMoreDemo({super.key});
@override
State<RefreshLoadMoreDemo> createState() => _RefreshLoadMoreDemoState();
}
class _RefreshLoadMoreDemoState extends State<RefreshLoadMoreDemo> {
late ScrollController _scrollController;
List<String> _dataList = [];
int _page = 1; // 当前页码
bool _isLoading = false; // 是否正在加载
@override
void initState() {
super.initState();
_scrollController = ScrollController();
// 初始化加载数据
_loadData();
// 监听滚动(上拉加载)
_scrollController.addListener(() {
// 到达底部且不在加载中
if (_scrollController.offset + _scrollController.position.viewportDimension
>= _scrollController.position.maxScrollExtent - 10 && !_isLoading) {
_loadMore();
}
});
}
// 加载数据(模拟网络请求)
Future<void> _loadData({bool isRefresh = false}) async {
if (isRefresh) {
_page = 1;
_dataList.clear();
}
setState(() {
_isLoading = true;
});
// 模拟网络延迟
await Future.delayed(Duration(seconds: 1));
// 模拟数据
List<String> newData = List.generate(25, (index) => "数据 ${(_page - 1) * 10 + index + 1}");
setState(() {
_dataList.addAll(newData);
_isLoading = false;
});
}
// 下拉刷新
Future<void> _onRefresh() async {
await _loadData(isRefresh: true);
}
// 上拉加载更多
Future<void> _loadMore() async {
if (_isLoading) return;
setState(() {
_page++;
_isLoading = true;
});
await _loadData();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
controller: _scrollController,
itemCount: _dataList.length + 1,
itemBuilder: (context, index) {
if (index == _dataList.length) {
return _isLoading
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink();
}
return ListTile(title: Text(_dataList[index]));
},
),
);
}
}
注意事项
- 上拉加载时需加
_isLoading锁,避免重复请求; - 模拟网络请求时,需处理异常(如请求失败后重置加载状态);
- RefreshIndicator 仅支持垂直滚动组件,水平滚动需自定义刷新组件。
三、综合应用案例
需求说明
实现一个“商品列表页”,包含以下功能:
- 支持网格/列表布局切换;
- 下拉刷新获取最新商品;
- 上拉加载更多商品;
- 滚动监听,显示滚动位置,支持一键返回顶部;
- 列表项带分隔线,网格项有间距。
完整代码
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "分页列表与网格综合案例",
theme: ThemeData(primarySwatch: Colors.blue),
home: ProductListPage(),
);
}
}
class ProductListPage extends StatefulWidget {
@override
_ProductListPageState createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
late ScrollController _scrollController;
List<Map<String, dynamic>> _productList = [];
int _page = 1;
bool _isLoading = false;
bool _isGridView = false; // 是否网格布局
double _scrollOffset = 0.0;
// 模拟商品数据
Future<void> _loadProducts({bool isRefresh = false}) async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
// 模拟网络请求
await Future.delayed(Duration(seconds: 1));
List<Map<String, dynamic>> newProducts = List.generate(
10,
(index) => {
"id": (_page - 1) * 10 + index + 1,
"name": "商品 ${(_page - 1) * 10 + index + 1}",
"price": "${(99 + index) * _page}",
},
);
setState(() {
if (isRefresh) {
_productList = newProducts;
_page = 1;
} else {
_productList.addAll(newProducts);
}
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("加载失败:$e")),
);
}
}
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_loadProducts();
// 滚动监听
_scrollController.addListener(() {
setState(() {
_scrollOffset = _scrollController.offset;
// 上拉加载更多
if (_scrollController.offset + _scrollController.position.viewportDimension
>= _scrollController.position.maxScrollExtent - 10 && !_isLoading) {
_loadMore();
}
});
});
}
// 下拉刷新
Future<void> _onRefresh() async => await _loadProducts(isRefresh: true);
// 上拉加载更多
Future<void> _loadMore() async {
setState(() => _page++);
await _loadProducts();
}
// 滚动到顶部
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: Duration(milliseconds: 500),
curve: Curves.ease,
);
}
// 切换布局
void _toggleLayout() => setState(() => _isGridView = !_isGridView);
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// 构建列表项
Widget _buildListItem(Map<String, dynamic> product) {
return ListTile(
leading: Icon(Icons.shopping_bag, color: Colors.blue),
title: Text(product["name"]),
subtitle: Text("价格:¥${product["price"]}"),
trailing: Text("ID: ${product["id"]}"),
);
}
// 构建网格项
Widget _buildGridItem(Map<String, dynamic> product) {
return Container(
margin: EdgeInsets.all(8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200]!),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.shopping_bag, color: Colors.blue, size: 32),
SizedBox(height: 8),
Text(product["name"], style: TextStyle(fontSize: 16)),
SizedBox(height: 4),
Text("¥${product["price"]}", style: TextStyle(color: Colors.red)),
Align(
alignment: Alignment.bottomRight,
child: Text("ID: ${product["id"]}", style: TextStyle(fontSize: 12, color: Colors.grey)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("商品列表(${_isGridView ? "网格" : "列表"})"),
actions: [
IconButton(
icon: Icon(_isGridView ? Icons.list : Icons.grid_view),
onPressed: _toggleLayout,
),
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: _scrollOffset > 100 ? _scrollToTop : null,
),
],
),
body: Column(
children: [
// 滚动状态提示
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.grey[100],
child: Text(
"滚动位置:${_scrollOffset.toStringAsFixed(0)}px | 当前页码:$_page",
style: TextStyle(fontSize: 14),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: _isGridView
? // 网格布局
GridView.builder(
controller: _scrollController,
itemCount: _productList.length + (_isLoading ? 1 : 0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.9,
),
itemBuilder: (context, index) {
if (index == _productList.length) {
return Center(child: CircularProgressIndicator());
}
return _buildGridItem(_productList[index]);
},
)
: // 列表布局(带分隔线)
ListView.separated(
controller: _scrollController,
itemCount: _productList.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == _productList.length) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildListItem(_productList[index]);
},
separatorBuilder: (context, index) => Divider(
color: Colors.grey[200],
height: 1,
),
),
),
),
],
),
);
}
}
功能说明
- 布局切换:点击右上角图标可切换列表/网格布局;
- 下拉刷新:顶部下拉触发刷新,重置页码并加载第一页数据;
- 上拉加载:滚动到底部自动加载下一页数据;
- 滚动控制:显示滚动位置,点击向上箭头可平滑返回顶部;
- 异常处理:模拟请求失败时显示 SnackBar 提示;
- 性能优化:所有列表/网格均使用懒加载,避免性能问题。
开发注意事项
- 列表/网格项中避免复杂计算和嵌套滚动,防止卡顿;
- 网络请求需处理异常,加载状态需合理展示;
- ScrollController 必须销毁,懒加载组件需指定 itemCount(无限列表除外)。