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 createState() => _CacheImageState(); } class _CacheImageState extends State { 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 _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(); }, ); } }