Chrome DevTools network Tab

example video

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

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

在实验时,我碰巧查看了Chrome DevTools中的网络选项卡,并注意到图像是通过网络请求而不是从缓存进入的.CachedNetworks Image似乎功能不正常.

这是我关于cachedNetworks Image的代码.

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 web的应用程序.

cached_network_image的文档如下:

CachedNetworks Image和CachedNetworks Image Provider都对Web的支持最低.目前它不包括缓存.

也就是说,您随时可以创建自己的小部件来解决这个问题.

这是一个小部件的简单实现,该小部件通过发出http获取请求来缓存网络图像,并将URL和生成的字节存储在哈希图中.如果您再次查找相同的网址,它只会从哈希图中提取字节,而不是发出另一个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(),
          ),
      },
    );
  }
}

Flutter相关问答推荐

从底部开始固定,然后使其可拖曳Flutter

Flutter 对齐:不适用于多行文字

从.gitignore中排除.fvm/fvm_config.json

如何将带有参数的函数传递给FIFTH中的自定义小部件

从图标启动器打开时,VSCode未检测到Web设备

Flutter:我的应用程序是否需要包含退款购买?

Flutter - Stripe:类型List 不是类型Map 的子类型? method_channel_stripe.dart 中的错误:263

如何动态获取任何小部件的像素?

在我使用 flutter_riverpod stateprovider 重新保存代码之前,UI 主题状态不会更新

像图像一样Flutter 自定义标签

我怎样才能消除我的 Column 子元素之间的差距?

Flutter:如何从运行时创建的列表中访问单个子部件的值

Paypal 支付网关添加带有 magento 2 网络详细信息用户名、密码、签名 Magento 2 的 flutter 应用程序?

Flutter - 根据从第一个 DropdownButtonForm 中 Select 的内容在第二个 DropdownButton 上显示选项

Flutter Serverpod 在示例项目上返回 400 错误

如何在 Flutter 中将 ScaffoldMessenger 中的 Snackbar 显示为 Widget?

如何按值对列表中的 map 进行排序

Flutter NavigationBar 忽略我的背景 colored颜色

如何在 Flutter 中的页面加载上的 Listview 上应用自动滚动

如何从我的Flutter 项目中删除错误?