194 lines
6.0 KiB
Dart
194 lines
6.0 KiB
Dart
import 'dart:io';
|
||
import 'dart:convert';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:crypto/crypto.dart';
|
||
import 'package:path/path.dart' as p;
|
||
|
||
/// 一个支持自动 Dio 缓存的 Image 组件
|
||
/// 用法完全对齐 Image.network(url)
|
||
class CacheImage extends StatefulWidget {
|
||
final String url;
|
||
final double? width;
|
||
final double? height;
|
||
final BoxFit? fit;
|
||
final double scale;
|
||
final AlignmentGeometry alignment;
|
||
final ImageRepeat repeat;
|
||
final FilterQuality filterQuality;
|
||
final Color? color;
|
||
final BlendMode? colorBlendMode;
|
||
final ImageLoadingBuilder? loadingBuilder;
|
||
final ImageErrorWidgetBuilder? errorBuilder;
|
||
final bool matchTextDirection;
|
||
|
||
// 使用位置参数接收 url,其余为可选命名参数
|
||
const CacheImage.network(
|
||
this.url, {
|
||
Key? key,
|
||
this.width,
|
||
this.height,
|
||
this.fit,
|
||
this.scale = 1.0,
|
||
this.alignment = Alignment.center,
|
||
this.repeat = ImageRepeat.noRepeat,
|
||
this.filterQuality = FilterQuality.low,
|
||
this.color,
|
||
this.colorBlendMode,
|
||
this.loadingBuilder,
|
||
this.errorBuilder,
|
||
this.matchTextDirection = false,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<CacheImage> createState() => _CacheImageState();
|
||
}
|
||
|
||
class _CacheImageState extends State<CacheImage> {
|
||
File? _localFile;
|
||
bool _isInit = false;
|
||
|
||
/// 统一日志工具(支持发布/调试环境)
|
||
void _log(String message, {bool isError = false}) {
|
||
// 发布环境只打印错误日志,调试环境全部打印
|
||
if (kReleaseMode) {
|
||
if (isError) {
|
||
print('[CacheImage-ERROR] $message');
|
||
}
|
||
} else {
|
||
final tag = isError ? '[CacheImage-ERROR]' : '[CacheImage-INFO]';
|
||
print('$tag $message');
|
||
}
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_log('图片组件初始化,URL: ${widget.url}');
|
||
_handleCache();
|
||
}
|
||
|
||
// 当外部传入的 url 发生变化时,重新触发缓存逻辑
|
||
@override
|
||
void didUpdateWidget(CacheImage oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.url != widget.url) {
|
||
_log('URL 发生变化,重新加载缓存\n旧URL: ${oldWidget.url}\n新URL: ${widget.url}');
|
||
_handleCache();
|
||
}
|
||
}
|
||
|
||
Future<void> _handleCache() async {
|
||
try {
|
||
_log('开始处理图片缓存逻辑,URL: ${widget.url}');
|
||
|
||
// 1. 生成唯一文件名 (MD5)
|
||
final String fileName = md5.convert(utf8.encode(widget.url)).toString();
|
||
_log('生成文件MD5: $fileName');
|
||
|
||
// 获取临时目录 (建议缓存放在临时目录,由系统根据空间决定是否清理)
|
||
final Directory cacheDir = await getTemporaryDirectory();
|
||
final File file = File(p.join(cacheDir.path, 'image_cache', fileName));
|
||
_log('本地缓存路径: ${file.path}');
|
||
|
||
// 2. 检查本地是否存在
|
||
if (await file.exists()) {
|
||
_log('✅ 本地缓存已存在,直接加载本地文件');
|
||
if (mounted) {
|
||
setState(() {
|
||
_localFile = file;
|
||
_isInit = true;
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 3. 本地不存在,使用 Dio 下载
|
||
_log('ℹ️ 本地无缓存,开始从网络下载图片');
|
||
final dio = Dio();
|
||
final response = await dio.get(
|
||
widget.url,
|
||
options: Options(responseType: ResponseType.bytes),
|
||
);
|
||
_log('✅ 图片下载完成,数据大小: ${response.data.length} bytes');
|
||
|
||
// 4. 尝试保存到本地 (Quiet Mode: 失败仅打印,不抛出异常)
|
||
try {
|
||
await file.parent.create(recursive: true);
|
||
await file.writeAsBytes(response.data);
|
||
_log('✅ 图片成功保存到本地缓存');
|
||
if (mounted) {
|
||
setState(() {
|
||
_localFile = file;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
_log('❌ 本地保存失败 (不影响显示): $e', isError: true);
|
||
}
|
||
} catch (e) {
|
||
_log('❌ 下载或逻辑处理失败: $e', isError: true);
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _isInit = true);
|
||
}
|
||
_log('🏁 图片缓存逻辑执行完成');
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 如果本地文件已就绪,直接加载本地文件
|
||
if (_localFile != null) {
|
||
_log('使用本地缓存图片渲染');
|
||
return Image.file(
|
||
_localFile!,
|
||
key: widget.key,
|
||
width: widget.width,
|
||
height: widget.height,
|
||
fit: widget.fit,
|
||
alignment: widget.alignment,
|
||
repeat: widget.repeat,
|
||
filterQuality: widget.filterQuality,
|
||
color: widget.color,
|
||
colorBlendMode: widget.colorBlendMode,
|
||
matchTextDirection: widget.matchTextDirection,
|
||
// 如果文件读取过程中出错,尝试回退到网络或报错组件
|
||
errorBuilder: widget.errorBuilder ?? (context, error, stack) {
|
||
_log('❌ 本地图片加载失败,回退到网络加载', isError: true);
|
||
return _buildNetworkImage();
|
||
},
|
||
);
|
||
}
|
||
|
||
// 默认或下载中,渲染网络图片
|
||
_log('使用网络图片渲染(加载中/无缓存)');
|
||
return _buildNetworkImage();
|
||
}
|
||
|
||
Widget _buildNetworkImage() {
|
||
return Image.network(
|
||
widget.url,
|
||
key: widget.key,
|
||
width: widget.width,
|
||
height: widget.height,
|
||
fit: widget.fit,
|
||
scale: widget.scale,
|
||
alignment: widget.alignment,
|
||
repeat: widget.repeat,
|
||
filterQuality: widget.filterQuality,
|
||
color: widget.color,
|
||
colorBlendMode: widget.colorBlendMode,
|
||
matchTextDirection: widget.matchTextDirection,
|
||
loadingBuilder: widget.loadingBuilder,
|
||
errorBuilder: (context, error, stack) {
|
||
_log('❌ 网络图片加载失败: $error', isError: true);
|
||
if (widget.errorBuilder != null) {
|
||
return widget.errorBuilder!(context, error, stack);
|
||
}
|
||
return const SizedBox();
|
||
},
|
||
);
|
||
}
|
||
} |