Flutter中如何避免ListView Builder加载新数据时刷新整个列表
解决方案:实现列表增量加载且避免全量刷新
核心问题分析
你的列表刷新问题主要来自两个点:
setState触发后,ListView.builder会重新构建所有可见列表项,而CustomBottomBigCard中的FutureBuilder每次都会重新执行传入的imgUrlFuture,导致图片反复加载、卡片闪烁。- 当前的加载触发逻辑没有防重复请求控制,滚动时可能多次调用
fetchData。
以下是针对性的修改方案:
1. 缓存图片请求Future,避免重复执行
将CustomBottomBigCard改为Stateful组件,在initState中缓存图片URL的Future,防止每次卡片构建时重新发起请求:
class CustomBottomBigCard extends StatefulWidget { const CustomBottomBigCard({ Key? key, required this.watchID, required this.screenWidth, required this.walletWatch, required this.imgUrlFuture, }) : super(key: key); final int watchID; final double screenWidth; final WalletWatch walletWatch; final Future<String> imgUrlFuture; @override State<CustomBottomBigCard> createState() => _CustomBottomBigCardState(); } class _CustomBottomBigCardState extends State<CustomBottomBigCard> { late Future<String> _cachedImgFuture; @override void initState() { super.initState(); // 仅在初始化时缓存Future,避免重复执行 _cachedImgFuture = widget.imgUrlFuture; } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.of(context).pushNamed(WatchScreen.id, arguments: Watch( watchId: widget.walletWatch.watchId, condition: widget.walletWatch.condition, numberOfShares: widget.walletWatch.numberOfShares, retailPrice: widget.walletWatch.retailPrice, initialPrice: widget.walletWatch.initialPrice, actualPrice: widget.walletWatch.actualPrice, dialcolor: widget.walletWatch.dialcolor, year: widget.walletWatch.year, imageuri: widget.walletWatch.imageuri, description: widget.walletWatch.description, modelTypeId: widget.walletWatch.modelTypeId, modelType: widget.walletWatch.modelType, )); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 7), padding: const EdgeInsets.only(top: 10, bottom: 10), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.black26, width: 1), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 2, offset: Offset(3, 3))], borderRadius: const BorderRadius.all(Radius.circular(7)), ), child: Row( children: [ Container( padding: EdgeInsets.symmetric(horizontal: widget.screenWidth * 0.04), child: Column( children: [ FutureBuilder<String>( future: _cachedImgFuture, // 使用缓存的Future builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } else if (snapshot.hasData) { return ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(7)), child: Image.network( snapshot.data!, width: widget.screenWidth * 0.3, height: widget.screenWidth * 0.3, fit: BoxFit.cover, ), ); } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { return const Text('Image not found'); } }, ), SizedBox(height: widget.screenWidth * 0.05), Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(3)), color: (widget.walletWatch.increaseRate >= 0) ? Colors.lightGreen : Colors.red, ), child: Text('${widget.walletWatch.increaseRate}%'), ), ], ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.walletWatch.modelType.model.brandname, style: TextStyle( color: Colors.black38, height: 1, fontSize: widget.screenWidth * 0.05, fontFamily: 'Bebas', ), ), Text( widget.walletWatch.modelType.model.modelname, style: TextStyle( color: Colors.black87, height: 1, fontSize: widget.screenWidth * 0.055, fontFamily: 'Bebas', ), ), Text('Reference: ${widget.walletWatch.modelType.reference}'), Text('Retail Price: ${formatAmountFromDouble(widget.walletWatch.retailPrice)}€'), SizedBox(height: widget.screenWidth * 0.02), Text('Owned Shares: ${widget.walletWatch.owned}/${widget.walletWatch.numberOfShares}'), Text('Initial Price: ${formatAmountFromDouble(widget.walletWatch.initialPrice)}€'), Text('Actual Price: ${formatAmountFromDouble(widget.walletWatch.actualPrice)}€'), ], ), ], ), ), ); } }
2. 优化加载触发逻辑,防止重复请求
修改fetchData方法和列表构建逻辑,增加加载状态控制,避免滚动时多次发起请求:
// 修改状态类中的fetchData方法 Future<void> fetchData() async { if (_loading) return; // 已有请求正在进行时直接返回 setState(() => _loading = true); try { var req = await getUserWalletWatches(12, _pageNumber, _numberOfPostsPerRequest); setState(() { _cards.addAll(req); // 用addAll替代循环add,提升效率 _isLastPage = req.length < _numberOfPostsPerRequest; _pageNumber += 1; }); } catch (e) { setState(() => _error = true); } finally { setState(() => _loading = false); } } // 修改buildCardsView方法 Widget buildCardsView() { return ListView.builder( itemCount: _cards.length + (_isLastPage ? 0 : 1), itemBuilder: (context, index) { // 仅在到达列表末尾、非最后一页、且未加载时触发请求 if (index == _cards.length && !_isLastPage && !_loading) { fetchData(); } // 渲染加载占位项 if (index == _cards.length) { return const Center(child: CircularProgressIndicator()); } final WalletWatch card = _cards[index]; return Padding( padding: const EdgeInsets.all(15.0), child: CustomBottomBigCard( watchID: card.watchId, screenWidth: widget.width, walletWatch: card, imgUrlFuture: getDownloadURL(card.imageuri), ), ); ); }
3. 可选:保持列表项状态,避免滑出后重建
如果希望列表项滑出视口再返回时不重新构建,可以给_CustomBottomBigCardState混入AutomaticKeepAliveClientMixin:
class _CustomBottomBigCardState extends State<CustomBottomBigCard> with AutomaticKeepAliveClientMixin { late Future<String> _cachedImgFuture; @override bool get wantKeepAlive => true; // 开启状态保持 @override void initState() { super.initState(); _cachedImgFuture = widget.imgUrlFuture; } @override Widget build(BuildContext context) { super.build(context); // 必须调用super.build // 原有build逻辑不变 } }
内容的提问来源于stack exchange,提问作者Geremia Moretti
相关产品推荐
相关产品推荐

