正如您在示例视频中看到的,当从左侧空间中选择一个类别时,相应的项目就会显示在屏幕上。主要问题是切换类别时发生的延迟,即使图像已经加载和缓存。
我相信这种延迟是由从内存中检索缓存图像所需的时间引起的。我尝试了多种方法来预加载小部件,但所有尝试都失败了。
在实验时,我偶然查看了 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 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(),
),
},
);
}
}