
列表是App中最常见的UI组件,商品列表、消息列表、搜索结果等都是列表。当列表数据量大时,性能优化就变得很重要。今天我们来讲解列表性能优化的实现方式。
列表性能问题的来源
列表性能问题主要来自两个方面:一是创建了太多Widget,二是图片加载消耗资源。优化的核心思路是懒加载和缓存。Flutter的渲染机制决定了Widget数量直接影响内存占用和渲染速度,所以控制Widget数量是优化的关键。
使用ListView.builder
dart
// 不推荐:一次性创建所有Widget
ListView(
children: products.map((p) => ProductCard(product: p)).toList(),
)
// 推荐:懒加载,只创建可见区域的Widget
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
普通ListView会一次性创建所有子Widget,如果有1000条数据就创建1000个Widget,内存直接爆炸。ListView.builder采用懒加载策略,只创建屏幕可见区域的Widget,滚动时动态创建新的、销毁离开屏幕的。这是列表优化最基本也是最重要的一步。
使用GridView.builder
商品列表用网格布局:
dart
GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.65,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: _products.length,
itemBuilder: (context, index) => _buildProductCard(_products[index]),
)
GridView.builder和ListView.builder原理一样,都是懒加载。gridDelegate定义网格的布局规则,crossAxisCount设置每行显示2个,childAspectRatio是宽高比,spacing是间距。这些参数都用const修饰,避免每次build都创建新对象。
使用SliverGrid
在CustomScrollView中使用SliverGrid:
dart
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _buildBanner()),
SliverToBoxAdapter(child: _buildCategories()),
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.65,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildProductCard(_products[index]),
childCount: _products.length,
),
),
),
],
)
首页通常有banner、分类导航、商品列表等多个部分,用CustomScrollView可以把它们组合成一个可滚动的整体。SliverChildBuilderDelegate实现懒加载,效果和GridView.builder一样。这种写法更灵活,可以在列表前后插入任意内容。
图片缓存
使用cached_network_image缓存网络图片:
dart
CachedNetworkImage(
imageUrl: product['image'],
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[200],
child: Icon(Icons.image, size: 60, color: Colors.grey[400]),
),
)
网络图片是列表性能的大敌,每次滚动都重新下载会很卡。CachedNetworkImage会把下载的图片缓存到本地,下次显示直接从缓存读取。placeholder在加载时显示占位内容,errorWidget在加载失败时显示错误提示,用户体验更好。
图片尺寸优化
加载适合显示尺寸的图片,不要加载原图:
dart
// 假设服务器支持图片尺寸参数
String getImageUrl(String url, {int width = 200}) {
return '$url?w=$width';
}
CachedNetworkImage(
imageUrl: getImageUrl(product['image'], width: 200),
// ...
)
商品卡片可能只有200像素宽,但原图可能是2000像素。加载原图不仅浪费流量,还会占用大量内存。让服务器返回合适尺寸的图片,或者使用CDN的图片处理功能,能大幅减少资源消耗。
使用const构造函数
能用const的地方都用const:
dart
// 不推荐
Container(
padding: EdgeInsets.all(16),
child: Text('文字'),
)
// 推荐
Container(
padding: const EdgeInsets.all(16),
child: const Text('文字'),
)
const对象在编译期就创建好了,运行时直接复用,不需要每次build都重新创建。这个优化看起来微不足道,但在列表中累积起来效果很明显。养成习惯,能加const的地方都加上。
避免在build中创建对象
dart
// 不推荐:每次build都创建新的列表
@override
Widget build(BuildContext context) {
final categories = [
{'icon': Icons.phone_android, 'name': '数码'},
{'icon': Icons.checkroom, 'name': '服饰'},
// ...
];
return // ...
}
// 推荐:在State中定义,只创建一次
class _HomePageState extends State<HomePage> {
final List<Map<String, dynamic>> _categories = [
{'icon': Icons.phone_android, 'name': '数码'},
{'icon': Icons.checkroom, 'name': '服饰'},
// ...
];
@override
Widget build(BuildContext context) {
return // ...
}
}
build方法可能会被频繁调用,每次调用都创建新对象会产生大量垃圾回收压力。把不变的数据定义在State中,或者用final修饰,只创建一次就够了。
使用RepaintBoundary
对于复杂的Widget,用RepaintBoundary隔离重绘:
dart
RepaintBoundary(
child: _buildProductCard(product),
)
Flutter默认会把相邻的Widget合并到一个图层一起绘制,一个Widget变化可能导致整个区域重绘。RepaintBoundary创建独立的图层,内部变化不会影响外部,外部变化也不会导致内部重绘。对于复杂的商品卡片,这个优化很有效。
使用AutomaticKeepAliveClientMixin
在PageView或TabBarView中保持页面状态:
dart
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return // ...
}
}
默认情况下,切换Tab时不可见的页面会被销毁,再切回来要重新加载。使用AutomaticKeepAliveClientMixin可以保持页面状态,滚动位置、加载的数据都会保留。注意build方法里必须调用super.build(context)。
分页加载
不要一次加载所有数据,分页加载:
dart
class _HomePageState extends State<HomePage> {
List<Map<String, dynamic>> _products = [];
int _page = 1;
bool _hasMore = true;
bool _isLoadingMore = false;
Future<void> _loadMoreProducts() async {
if (_isLoadingMore || !_hasMore) return;
setState(() => _isLoadingMore = true);
try {
_page++;
final data = await Api.getProducts(page: _page);
setState(() {
_products.addAll(data);
_hasMore = data.length >= 20;
_isLoadingMore = false;
});
} catch (e) {
_page--;
setState(() => _isLoadingMore = false);
}
}
}
一次加载1000条数据,不管是网络传输还是内存占用都是灾难。分页加载每次只请求20条,滚动到底部时再加载下一页。_isLoadingMore防止重复请求,_hasMore判断是否还有更多数据,加载失败时回退页码。
使用itemExtent
如果列表项高度固定,指定itemExtent:
dart
ListView.builder(
itemExtent: 100, // 每项高度100
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
指定itemExtent后,Flutter不需要调用每个子Widget的layout来计算高度,可以直接通过数学计算确定哪些项在可见区域。这对于长列表的滚动性能提升很明显。
小结
这篇讲解了列表性能优化的实现方式,包括使用builder懒加载、图片缓存、const构造函数、RepaintBoundary、分页加载等。性能优化能让App更流畅,提升用户体验。记住优化的核心思路:减少Widget数量、减少重绘、减少内存占用。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net