Flutter中CachedNetworkImage缓存失效,分类切换图片加载延迟问题
Flutter CachedNetworkImage缓存失效与页面切换图片延迟问题
问题现象
- 切换分类页面时,已加载过的图片仍出现明显显示延迟
- 调试发现图片未从缓存读取,而是重复发起网络请求
- 尝试预加载Widget方法无效
相关代码
CachedNetworkImage使用代码
Container( decoration: BoxDecoration( border: Border.all( color: Colors.black, width: 2.0), borderRadius: BorderRadius.circular(10.0)), child: ClipRRect( borderRadius: BorderRadius.circular(8.0), child: CachedNetworkImage( key: ValueKey(tileData['imageCode']), cacheKey: tileData['imageCode'], imageUrl: widget.menuData['imageCode'][tileData['imageCode']], cacheManager: CustomCacheManager.instance, fit: BoxFit.cover))) class CustomCacheManager { static const key = 'imageCache'; static CacheManager instance = CacheManager( Config( key, stalePeriod: const Duration(days: 1), maxNrOfCacheObjects: 100, // 최대 캐시 객체 수를 100개로 제한 repo: JsonCacheInfoRepository(databaseName: key), fileService: HttpFileService(), ), ); }
完整相关代码
void _menuPreRender() { _menuWidgets.clear(); for (Map<String, dynamic> category in _menuData['toManageList']) { _menuWidgets[category['name']] = MenuView( key: ValueKey(category['name']), menuData: _menuData, category: category['name'], updateCallback: (int selectedMenuIndex) { return () { setState(() { _selectedMenu = selectedMenuIndex; _mainAreaWidget = _menuEdit(isEdit: true); }); }; }, setManageMenuData: (Map<String, dynamic> data) { _setManageMenuData(data); }); } } /* ... */ class MenuView extends StatefulWidget { const MenuView( {super.key, required this.menuData, required this.category, required this.updateCallback, required this.setManageMenuData}); final String category; final Map<String, dynamic> menuData; final Function(int selectedMenuIndex) updateCallback; final Function(Map<String, dynamic>) setManageMenuData; @override State<MenuView> createState() => _MenuViewState(); } class _MenuViewState extends State<MenuView> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override void didChangeDependencies() { super.didChangeDependencies(); for (var menu in widget.menuData['toManage'][widget.category]['menu']) { if (menu['imageCode'] != null && menu['imageCode'] != '') { precacheImage( NetworkImage(widget.menuData['imageCode'][menu['imageCode']]), context); } } } @override Widget build(BuildContext context) { super.build(context); print('category: ${widget.category}'); double screenHeight = MediaQuery.of(context).size.height; return ReorderableListView.builder( buildDefaultDragHandles: false, itemCount: widget.menuData['toManage'][widget.category]['menu'].length, itemBuilder: (context, index) { Map<String, dynamic> tileData = widget.menuData['toManage'][widget.category]['menu'][index]; return ListTile( key: Key('$index'), title: SizedBox( height: screenHeight * 0.15, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tileData['name'], style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20), ), const SizedBox(height: 5), Text( tileData['description'], style: const TextStyle(fontSize: 15, color: Colors.grey), ), const SizedBox(height: 5), Text( '${tileData['price']}원', style: const TextStyle(fontSize: 20), ), ], ), SizedBox( width: screenHeight * 0.14, height: screenHeight * 0.14, child: tileData['imageCode'] != null ? Container( decoration: BoxDecoration( border: Border.all( color: Colors.black, width: 2.0), borderRadius: BorderRadius.circular(10.0)), child: ClipRRect( borderRadius: BorderRadius.circular(8.0), child: CachedNetworkImage( key: ValueKey(tileData['imageCode']), cacheKey: tileData['imageCode'], imageUrl: widget.menuData['imageCode'] [tileData['imageCode']], cacheManager: CustomCacheManager.instance, fit: BoxFit.cover))) : const SizedBox.shrink()), ], ), ), onTap: widget.updateCallback(index), trailing: Container( width: screenHeight * 0.08, alignment: Alignment.bottomCenter, child: ReorderableDragStartListener( index: index, child: Icon(Icons.drag_handle_rounded, size: screenHeight * 0.08)), )); }, onReorder: (oldIndex, newIndex) { List<Map<String, dynamic>> menuList = widget.menuData['toManage'][widget.category]['menu']; setState(() { if (oldIndex < newIndex) { newIndex -= 1; } final Map<String, dynamic> item = menuList.removeAt(oldIndex); menuList.insert(newIndex, item); }); widget.setManageMenuData(widget.menuData['toManage']); }, ); } }
解决方案
1. 调整Flutter内存缓存上限
Flutter默认ImageCache内存限制较小,可在启动时增大缓存容量,避免图片被频繁回收:
void main() { // 设置内存缓存为100MB,根据实际情况调整 PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 100; runApp(MyApp()); }
2. 替换预加载方式为CachedNetworkImageProvider
原预加载使用NetworkImage未关联自定义缓存管理器,改为用CachedNetworkImageProvider确保缓存一致性:
@override void didChangeDependencies() { super.didChangeDependencies(); for (var menu in widget.menuData['toManage'][widget.category]['menu']) { if (menu['imageCode'] != null && menu['imageCode'] != '') { final imageProvider = CachedNetworkImageProvider( widget.menuData['imageCode'][menu['imageCode']], cacheManager: CustomCacheManager.instance, cacheKey: menu['imageCode'], ); precacheImage(imageProvider, context); } } }
3. 优化CacheManager配置
增大缓存对象数量上限,避免因缓存容量不足导致旧图片被清理:
static CacheManager instance = CacheManager( Config( key, stalePeriod: const Duration(days: 1), maxNrOfCacheObjects: 500, // 调大缓存对象数量 repo: JsonCacheInfoRepository(databaseName: key), fileService: HttpFileService(), ), );
4. 修正ListTile的Key
原Key('$index')会在列表重排时导致Widget重建,改用基于图片唯一标识的Key,确保Widget复用:
return ListTile( key: ValueKey(tileData['imageCode'] ?? index), // ...其他代码 );
5. 配置CachedNetworkImage内存缓存尺寸
指定与显示容器匹配的缓存尺寸,减少内存占用并提高缓存命中率:
CachedNetworkImage( key: ValueKey(tileData['imageCode']), cacheKey: tileData['imageCode'], imageUrl: widget.menuData['imageCode'][tileData['imageCode']], cacheManager: CustomCacheManager.instance, fit: BoxFit.cover, // 匹配容器的宽高,避免缓存过大尺寸图片 memCacheWidth: (screenHeight * 0.14).toInt(), memCacheHeight: (screenHeight * 0.14).toInt(), )
内容的提问来源于stack exchange,提问作者한경완
相关产品推荐
相关产品推荐

