在 Flutter 中,CachedNetworkImage 不起作用(不从 CachedData 加载)

问题描述 投票:0回答:1

Chrome DevTools network Tab

示例视频

正如您在示例视频中看到的,当从左侧空间中选择一个类别时,相应的项目就会显示在屏幕上。主要问题是切换类别时发生的延迟,即使图像已经加载和缓存。

我相信这种延迟是由从内存中检索缓存图像所需的时间引起的。我尝试了多种方法来预加载小部件,但所有尝试都失败了。

在实验时,我偶然查看了 Chrome DevTools 中的网络选项卡,注意到图像是通过网络请求传入的,而不是来自缓存。 CachedNetworkImage 似乎无法正常运行。

这是我关于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, // Container 내부의 아이콘을 중앙에 배치
              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']);
      },
    );
  }
}

我想要的是已经加载的图像能够立即显示给用户,在切换类别时没有任何延迟。我相信一定有一种方法可以实现这一点,因为许多商业应用程序都支持类似的功能。

flutter dart caching
1个回答
0
投票

看来您正在开发 flutter web 应用程序。

cached_network_image的文档说明如下:

CachedNetworkImage 和 CachedNetworkImageProvider 对 Web 的支持都很少。目前它不包括缓存。

也就是说,您始终可以创建自己的小部件来解决这个问题。

这是一个小部件的简单实现,它通过发出 http get 请求并将 url 和结果字节存储在哈希图中来缓存网络图像。如果您再次查找相同的 url,它只会从 hashmap 中提取字节,而不是发出另一个 get 请求。这是一个过于简单的解决方案,因为它不会从缓存中删除图像,这可能被视为内存泄漏。

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;


class CustomCacheImage extends StatefulWidget {
  const CustomCacheImage(this.src, {this.height, this.width, super.key});

  final String src;
  final double? height;
  final double? width;

  @override
  State<CustomCacheImage> createState() => _CustomCacheImageState();
}

class _CustomCacheImageState extends State<CustomCacheImage> {
  static final Map<String, Uint8List> _cache = {};

  Uint8List? _cached;
  Future<Uint8List>? _future;

  @override
  void initState() {
    super.initState();
    _cached = _cache[widget.src]; // attempt to retrieve image from cache
    if (_cached == null) {
      // if image not in cache, fetch from network
      _future = http.get(Uri.parse(widget.src)).then((response) {
        Uint8List bytes = response.bodyBytes;
        _cache[widget.src] = bytes; // store image in cache
        return bytes;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _future,
      initialData: _cached,
      builder: (context, snapshot) => switch (snapshot) {
        // handle future error state
        AsyncSnapshot(hasError: true) => const Placeholder(),
        // handle future loading state
        AsyncSnapshot(hasData: false) => const Placeholder(),
        // handle future completed successfully
        AsyncSnapshot(hasData: true, :var data) => Image.memory(
            data!,
            width: widget.width,
            height: widget.height,
            // handle image invalid
            errorBuilder: (context, error, stackTrace) => const Placeholder(),
          ),
      },
    );
  }
}

下面是一个使用上述小部件的完整示例应用程序:

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

const animals = [
  'https://upload.wikimedia.org/wikipedia/commons/f/f7/Llamas%2C_Vernagt-Stausee%2C_Italy.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/9/9e/Giraffe_Mikumi_National_Park.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/3/37/African_Bush_Elephant.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/0/07/Didelphis_virginiana_with_young.JPG',
  'https://upload.wikimedia.org/wikipedia/commons/3/3e/Raccoon_in_Central_Park_%2835264%29.jpg',
];

const objects = [
  'https://upload.wikimedia.org/wikipedia/commons/8/81/AT%26T_push_button_telephone_western_electric_model_2500_dmg_black.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/a/ac/Plastic_Tuinstoel.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/0/06/ElectricBlender.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/e/ea/Magnifying_glass_with_focus_on_paper.png',
  'https://upload.wikimedia.org/wikipedia/commons/5/5d/Roller-skate.jpg',
  // these last 2 are intentionally errors
  'ewfwefewwfssd',
  'https://upload.wikimedia.org/wikipedia/commons/5/5d/nreijfoisejfisejfoif.jpg',
];

void main() {
  runApp(const MaterialApp(
    home: HomePage(),
  ));
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      initialIndex: 0,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Cache Demo'),
          bottom: const TabBar(
            tabs: [
              Tab(text: 'Animals'),
              Tab(text: 'Objects'),
            ],
          ),
        ),
        body: TabBarView(children: [
          ListView(children: [
            for (var link in animals)
              CustomCacheImage(link, height: 200, width: 200),
          ]),
          ListView(children: [
            for (var link in objects)
              CustomCacheImage(link, height: 200, width: 200),
          ]),
        ]),
      ),
    );
  }
}

class CustomCacheImage extends StatefulWidget {
  const CustomCacheImage(this.src, {this.height, this.width, super.key});

  final String src;
  final double? height;
  final double? width;

  @override
  State<CustomCacheImage> createState() => _CustomCacheImageState();
}

class _CustomCacheImageState extends State<CustomCacheImage> {
  static final Map<String, Uint8List> _cache = {};

  Uint8List? _cached;
  Future<Uint8List>? _future;

  @override
  void initState() {
    super.initState();
    _cached = _cache[widget.src]; // attempt to retrieve image from cache
    if (_cached == null) {
      // if image not in cache, fetch from network
      _future = http.get(Uri.parse(widget.src)).then((response) {
        Uint8List bytes = response.bodyBytes;
        _cache[widget.src] = bytes; // store image in cache
        return bytes;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _future,
      initialData: _cached,
      builder: (context, snapshot) => switch (snapshot) {
        // handle future error state
        AsyncSnapshot(hasError: true) => const Placeholder(),
        // handle future loading state
        AsyncSnapshot(hasData: false) => const Placeholder(),
        // handle future completed successfully
        AsyncSnapshot(hasData: true, :var data) => Image.memory(
            data!,
            width: widget.width,
            height: widget.height,
            // handle image invalid
            errorBuilder: (context, error, stackTrace) => const Placeholder(),
          ),
      },
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.