列表是移动 App 中最核心的 UI 组件。掌握 ListView、ScrollController、分页加载和下拉刷新,是构建流畅用户体验的基础。
一、ListView 的几种构建方式
1.1 ListView(直接构建,适合少量数据)
dart
// ❌ 一次性构建所有子 Widget(数据量大时会卡顿)
ListView(
children: allProducts.map((p) => ProductCard(product: p)).toList(),
)
1.2 ListView.builder(懒加载,推荐)
dart
// ✅ 按需构建可见区域内的子 Widget
ListView.builder(
itemCount: products.length,
itemExtent: 80, // 固定高度 → 性能更好(跳过 Layout 计算)
itemBuilder: (context, index) {
return ProductListTile(product: products[index]);
},
)
1.3 ListView.separated(带分割线)
dart
ListView.separated(
itemCount: products.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
return ProductListTile(product: products[index]);
},
)
1.4 GridView.builder(网格列表)
dart
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductGridCard(product: products[index]);
},
)
二、ScrollController
dart
class InfiniteListPage extends StatefulWidget { ... }
class _InfiniteListPageState extends State<InfiniteListPage> {
final _scrollController = ScrollController();
bool _showBackToTop = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
// 显示/隐藏"回到顶部"按钮
final showButton = _scrollController.offset > 500;
if (showButton != _showBackToTop) {
setState(() => _showBackToTop = showButton);
}
// ⭐ 滚动到底部时加载更多
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
// 滚动到指定位置
void _scrollToItem(int index) {
const itemHeight = 80.0;
_scrollController.animateTo(
index * itemHeight,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
}
三、分页加载(Pagination)
dart
class PaginatedProductList extends StatefulWidget { ... }
class _PaginatedProductListState extends State<PaginatedProductList> {
final List<Product> _products = [];
int _currentPage = 1;
bool _isLoading = false;
bool _hasMore = true;
@override
void initState() {
super.initState();
_loadPage(_currentPage);
}
Future<void> _loadPage(int page) async {
if (_isLoading || !_hasMore) return;
setState(() => _isLoading = true);
try {
final result = await ProductApi.fetchProducts(page: page, pageSize: 20);
setState(() {
_products.addAll(result.items);
_hasMore = result.hasMore;
_currentPage = page;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载失败:$e')),
);
} finally {
setState(() => _isLoading = false);
}
}
void _loadMore() => _loadPage(_currentPage + 1);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _products.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
// 最后一个 item 显示加载指示器
if (index == _products.length) {
_loadMore();
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return ProductListTile(product: _products[index]);
},
);
}
}
四、下拉刷新
4.1 RefreshIndicator(Material 风格)
dart
RefreshIndicator(
onRefresh: () async {
setState(() {
_currentPage = 1;
_products.clear();
_hasMore = true;
});
await _loadPage(1);
},
displacement: 50,
color: Theme.of(context).colorScheme.primary,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), // 确保可滚动才能下拉
itemCount: _products.length,
itemBuilder: (_, index) => ProductListTile(product: _products[index]),
),
)
4.2 CupertinoSliverRefreshControl(iOS 风格)
dart
CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 100,
refreshIndicatorExtent: 60,
onRefresh: () async {
await _refreshData();
},
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ProductListTile(product: _products[index]),
childCount: _products.length,
),
),
],
)
4.3 easy_refresh(功能完整,推荐)
yaml
dependencies:
easy_refresh: ^3.3.4
dart
EasyRefresh(
header: const ClassicHeader(
dragText: '下拉刷新',
armedText: '释放刷新',
readyText: '刷新中...',
processingText: '刷新中...',
processedText: '刷新完成',
),
footer: const ClassicFooter(
dragText: '上拉加载更多',
readyText: '加载中...',
processedText: '加载完成',
noMoreText: '没有更多了',
),
onRefresh: () async {
await _refresh();
},
onLoad: () async {
await _loadMore();
return _hasMore ? IndicatorResult.success : IndicatorResult.noMore;
},
child: ListView.builder(
itemCount: _products.length,
itemBuilder: (_, index) => ProductCard(product: _products[index]),
),
)
五、列表性能优化
5.1 指定 itemExtent(固定高度)
dart
// ❌ 不指定:Flutter 需要逐个计算每个 Item 高度
ListView.builder(
itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
)
// ✅ 指定 itemExtent:跳过 Layout 计算,直接定位
ListView.builder(
itemExtent: 72, // 每个 Item 高度固定 72
itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
)
// ✅ 或使用 prototypeItem(由第一个 Item 推断高度)
ListView.builder(
prototypeItem: const ListTile(title: Text('Prototype')),
itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
)
5.2 const + Key
dart
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ProductCard(
key: ValueKey(items[index].id), // ✅ 用 ValueKey 帮助 diff
product: items[index],
);
},
)
5.3 AutomaticKeepAlive(保持 Tab 内列表状态)
dart
class _ProductListState extends State<ProductList>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // 切换 Tab 后保留状态
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用 super.build
return ListView.builder(...);
}
}
5.4 缓存区域(cacheExtent)
dart
ListView.builder(
cacheExtent: 500, // 额外缓存 500 像素区域的 Item
// 默认 250,增大可减少滚动时白屏,但增加内存
itemBuilder: ...,
)
六、SliverAppBar + 复杂滚动布局
dart
CustomScrollView(
slivers: [
// 折叠头部
SliverAppBar(
expandedHeight: 250,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('商品列表'),
background: Image.network(bannerUrl, fit: BoxFit.cover),
stretchModes: const [StretchMode.zoomBackground],
),
actions: [IconButton(icon: const Icon(Icons.search), onPressed: _search)],
),
// 固定的吸顶 Tab 栏
SliverPersistentHeader(
pinned: true,
delegate: _StickyTabBarDelegate(
child: TabBar(
controller: _tabController,
tabs: const [Tab(text: '推荐'), Tab(text: '新品'), Tab(text: '热销')],
),
),
),
// 商品网格
SliverGrid(
delegate: SliverChildBuilderDelegate(
(_, index) => ProductCard(product: products[index]),
childCount: products.length,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
),
),
// 底部加载指示器
if (_isLoading)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
),
],
)
小结
| 技术点 | 推荐做法 |
|---|---|
| 少量数据 | ListView(children: [...]) |
| 大量数据 | ListView.builder + itemExtent |
| 分页加载 | 监听 ScrollController 或用 easy_refresh |
| 下拉刷新 | RefreshIndicator / EasyRefresh |
| 列表性能 | const + ValueKey + cacheExtent + itemExtent |
| 复杂滚动 | CustomScrollView + Sliver 系列 |
👉 下一节:2.8 对话框与弹窗系统