You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者한경완

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 12:59:55