Flutter中的网络请求图片存储为缓存,与定制删除本地缓存
1:封装请求图片函数
2:访问的图片都会转为本地缓存,当相同的请求url,会在本地调用图片
3:本地缓存管理【windows与andriod已经测试】【有页面】【有调用案例】
4:删除本地缓存
清理缓存页面(下方代码中已包括)
windows中显示图片-----------安卓中显示图片
这里还没有进行优化图片显示的宽高,圆角,请自行设置
打印日志(显示图片请求的获取过程与报错原因)
TuPianJiaZai 图片加载工具使用教程
注意事项
- imageUrl 可以为 null,此时会显示空白
- 图片会自动缓存到本地
- 支持自动重试3次
- 默认有加载动画和错误提示
- 支持所有标准图片格式
实际应用场景
- 商品展示卡片
- 用户头像
- 图片列表
- 背景图片
- Banner图片
1. 基本用法
1.1导入文件
import '../utils/get_images/tupianjiazai.dart';
dart
TuPianJiaZai.jiazaiTupian(
imageUrl: product.image,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
)
2. 完整参数说明
dart
TuPianJiaZai.jiazaiTupian(
// 必需参数
imageUrl: String?, // 图片URL,可以为null
// 可选参数
width: double?, // 显示宽度
height: double?, // 显示高度
fit: BoxFit, // 图片填充方式,默认BoxFit.cover
cacheWidth: int?, // 缓存图片宽度,用于优化内存
cacheHeight: int?, // 缓存图片高度,用于优化内存
placeholder: Widget?, // 加载时显示的占位Widget
errorWidget: Widget?, // 加载失败时显示的Widget
)
3. 使用案例
3.1 基础加载
dart
TuPianJiaZai.jiazaiTupian(
imageUrl: 'https://example.com/image.jpg',
width: 200,
height: 200,
)
3.2 自定义占位图和错误图
dart
TuPianJiaZai.jiazaiTupian(
imageUrl: imageUrl,
width: 300,
height: 200,
placeholder: const Center(
child: CircularProgressIndicator(),
),
errorWidget: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.error),Text('加载失败'),],
),
),
)
3.3 列表项中使用
dart
ListView.builder(
itemBuilder: (context, index) {
return TuPianJiaZai.jiazaiTupian(
imageUrl: imageUrls[index],
height: 150,
fit: BoxFit.cover,
cacheWidth: 600, // 优化缓存大小
cacheHeight: 400,
);
},
)
请自行在\lib\utils\get_images\文件夹中创建一下配置
D:\F\luichun\lib\utils\get_images\huancunguanli.dart
dart
import 'dart:io';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synchronized/synchronized.dart';
import 'logger.dart'; // 使用统一的日志管理器
import '../env_config.dart';// 本地进行开发时,使用 会对 localhost:10005 进行请求,但是安卓模拟器需要把localhost转换为 10.0.2.2
/// 完整工作流程:
/// 1.应用启动 -> 初始化缓存目录
/// 2.请求图片 -> 检查缓存 -> 返回缓存或null
/// 3.下载图片 -> 保存图片 -> 更新映射关系
/// 4.定期维护 -> 清理缓存/计算大小
/// 图片缓存管理器
/// 用于管理图片的本地缓存,减少重复的网络请求
class HuanCunGuanLi {
/// 单例模式
/// 使用工厂构造函数确保全局只有一个缓存管理器实例
/// 避免重复创建缓存目录和资源浪费
static final HuanCunGuanLi _instance = HuanCunGuanLi._internal();
/// 缓存目录
Directory? _cacheDir;
/// 初始化锁
final _lock = Lock();
/// 初始化标志
bool _isInitialized = false;
/// 持久化存储的键名
static const String _prefKey = 'image_cache_urls';
// 工厂构造函数
factory HuanCunGuanLi() {
return _instance;
}
// 私有构造函数
HuanCunGuanLi._internal();
/// 确保已初始化
Future<void> _ensureInitialized() async {
if (_isInitialized) return; // 快速检查
await _lock.synchronized(() async {
if (_isInitialized) return; // 双重检查
await init();
});
}
/// 初始化缓存目录
Future<void> init() async {
try {
final appDir = await getApplicationDocumentsDirectory();
final cacheDir = Directory('${appDir.path}/image_cache');
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
_cacheDir = cacheDir;
_isInitialized = true;
if (EnvConfig.isDevelopment) {
ImageLogger.logCacheInfo('缓存系统初始化完成: ${_cacheDir!.path}');
}
} catch (e) {
ImageLogger.logCacheError('缓存系统初始化失败', error: e);
rethrow;
}
}
/// 3异步获取缓存图片
/// 参数:
/// url: 图片的网络地址
/// 返回:
/// Uint8List?: 图片的二进制数据,不存在时返回null
/// 流程:
/// 1. 根据URL生成缓存键
/// 2. 查找本地缓存文件
/// 3. 返回缓存数据或null
Future<Uint8List?> huoquTupian(String url) async {
await _ensureInitialized();
try {
final cacheKey = _shengchengKey(url);
final cacheFile = File('${_cacheDir!.path}/$cacheKey');
if (await cacheFile.exists()) {
ImageLogger.logCacheDebug('从缓存加载图片', {'url': url});
return await cacheFile.readAsBytes();
}
return null;
} catch (e) {
ImageLogger.logCacheError('读取缓存图片失败', error: e);
return null;
}
}
/// 异步保存图片到缓存
/// [url] 图片URL
/// [imageBytes] 图片二进制数据
Future<void> baocunTupian(String url, Uint8List imageBytes) async {
await _ensureInitialized();
final cacheKey = _shengchengKey(url);
final cacheFile = File('${_cacheDir!.path}/$cacheKey');
await cacheFile.writeAsBytes(imageBytes);
await _baocunURLyingshe(url, cacheKey);
}
/// 生成缓存键
/// 使用MD5加密URL生成唯一标识
String _shengchengKey(String url) {
final bytes = utf8.encode(url);
final digest = md5.convert(bytes);
return digest.toString();
}
/// 4. URL 映射管理:
/// 保存URL映射关系
/// 实现:
/// 1. 获取SharedPreferences实例
/// 2. 读取现有映射
/// 3. 更新映射关系
/// 4. 序列化并保存
/// 使用 SharedPreferences 持久化存储 URL 映射关系
/// JSON 序列化保存映射数据
/// 异步操作避免阻塞主线程
/// 保存URL映射关系
Future<void> _baocunURLyingshe(String url, String cacheKey) async {
final prefs = await SharedPreferences.getInstance();
final Map<String, String> urlMap = await _huoquURLyingshe();
urlMap[url] = cacheKey;
await prefs.setString(_prefKey, jsonEncode(urlMap));
}
/// 获取URL映射关系
Future<Map<String, String>> _huoquURLyingshe() async {
final prefs = await SharedPreferences.getInstance();
final String? mapJson = prefs.getString(_prefKey);
if (mapJson != null) {
return Map<String, String>.from(jsonDecode(mapJson));
}
return {};
}
/// 5.缓存清理功能:
/// 清除所有缓存
/// 使用场景:
/// 1. 应用清理存储空间
/// 2. 图片资源更新
/// 3. 缓存出现问题时重置
/// 递归删除缓存目录
/// 清除 URL 映射数据
/// 清除所有缓存
Future<void> qingchuHuancun() async {
await _cacheDir!.delete(recursive: true);
await _cacheDir!.create();
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_prefKey);
}
///6 .缓存大小计算:
///- 异步遍历缓存目录
/// 累计所有文件大小
/// 使用 Stream 处理大目录
/// 获取缓存大小(字节)
Future<int> huoquHuancunDaxiao() async {
int size = 0;
await for (final file in _cacheDir!.list()) {
if (file is File) {
size += await file.length();
}
}
return size;
}
}
D:\F\luichun\lib\utils\get_images\logger.dart
dart
import 'package:logger/logger.dart';
/// 图片加载系统的日志管理器
class ImageLogger {
static final Logger _logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
errorMethodCount: 8,
lineLength: 120,
colors: true,
printEmojis: true,
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
),
);
// 缓存系统日志
static void logCacheInfo(String message) {
_logger.i('📦 $message');
}
static void logCacheError(String message, {dynamic error}) {
_logger.e('📦 $message', error: error);
}
static void logCacheDebug(String message, [Map<String, dynamic>? data]) {
if (data != null) {
_logger.d('📦 $message\n${_formatData(data)}');
} else {
_logger.d('📦 $message');
}
}
// 图片加载日志
static void logImageInfo(String message) {
_logger.i('🖼️ $message');
}
static void logImageError(String message, {dynamic error}) {
_logger.e('🖼️ $message', error: error);
}
static void logImageDebug(String message, [Map<String, dynamic>? data]) {
if (data != null) {
_logger.d('🖼️ $message\n${_formatData(data)}');
} else {
_logger.d('🖼️ $message');
}
}
static void logImageWarning(String message, [Map<String, dynamic>? data]) {
if (data != null) {
_logger.w('🖼️ $message\n${_formatData(data)}');
} else {
_logger.w('🖼️ $message');
}
}
// 格式化数据为字符串
static String _formatData(Map<String, dynamic> data) {
return data.entries
.map((e) => ' ${e.key}: ${e.value}')
.join('\n');
}
}
D:\F\luichun\lib\utils\get_images\qinglihuancun.dart
dart
// import 'package:flutter/material.dart';
import 'huancunguanli.dart';
import 'logger.dart';
/// 缓存清理管理器
class QingLiHuanCun {
static final HuanCunGuanLi _huancun = HuanCunGuanLi();
/// 清理所有缓存
static Future<void> qingliSuoyou() async {
try {
await _huancun.qingchuHuancun();
ImageLogger.logCacheInfo('缓存清理完成');
} catch (e) {
ImageLogger.logCacheError('缓存清理失败', error: e);
}
}
/// 获取当前缓存大小
static Future<String> huoquDaxiao() async {
try {
final size = await _huancun.huoquHuancunDaxiao();
// 转换为合适的单位
if (size < 1024) return '$size B';
if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(2)} KB';
return '${(size / (1024 * 1024)).toStringAsFixed(2)} MB';
} catch (e) {
ImageLogger.logCacheError('获取缓存大小失败', error: e);
return '未知';
}
}
/// 检查缓存大小并在超过阈值时清理
static Future<void> jianchaHeQingli() async {
try {
final size = await _huancun.huoquHuancunDaxiao();
// 如果缓存超过550MB,则清理
if (size > 550 * 1024 * 1024) {
await qingliSuoyou();
}
} catch (e) {
ImageLogger.logCacheError('缓存检查失败', error: e);
}
}
}
D:\F\luichun\lib\utils\get_images\qinglihuancundeanniu.dart
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
import 'qinglihuancun.dart';
/// 缓存配置
class CacheConfig {
// 警告阈值 (当缓存超过500MB时显示警告)
static const double warningThresholdMB = 500.0;
// 自动清理阈值
static const double autoCleanThresholdMB = 550.0;
// 动画时长
static const Duration animationDuration = Duration(milliseconds: 300);
// 提示显示时长
static const Duration snackBarDuration = Duration(seconds: 3);
// 刷新动画时长
static const Duration refreshAnimationDuration = Duration(milliseconds: 200);
}
/// 清理完成回调
typedef OnCleanComplete = void Function(bool success);
/// 缓存监听器
class CacheListener {
static final List<VoidCallback> _listeners = [];
static void addListener(VoidCallback listener) {
_listeners.add(listener);
}
static void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
static void notifyListeners() {
for (var listener in _listeners) {
listener();
}
}
}
/// 自动清理调度器
class AutoCleanScheduler {
static Timer? _timer;
// 每24小时自动检查一次
static void startSchedule() {
_timer?.cancel();
_timer = Timer.periodic(
const Duration(hours: 24),
(_) => QingLiHuanCun.jianchaHeQingli(),
);
}
static void stopSchedule() {
_timer?.cancel();
_timer = null;
}
}
/// 缓存管理页面
class CacheManagementScreen extends StatelessWidget {
const CacheManagementScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('缓存管理'),
elevation: 0,
),
body: const SingleChildScrollView(
child: QingLiHuanCunAnNiu(),
),
);
}
}
/// 缓存清理按钮组件
class QingLiHuanCunAnNiu extends StatefulWidget {
final OnCleanComplete? onCleanComplete;
const QingLiHuanCunAnNiu({
super.key,
this.onCleanComplete,
});
@override
State<QingLiHuanCunAnNiu> createState() => _QingLiHuanCunAnNiuState();
}
class _QingLiHuanCunAnNiuState extends State<QingLiHuanCunAnNiu> {
String _cacheSize = '计算中...';
bool _isClearing = false;
Timer? _autoCheckTimer;
DateTime? _lastClickTime;
bool _isDoubleClick = false;
@override
void initState() {
super.initState();
_initializeCache();
}
@override
void dispose() {
_autoCheckTimer?.cancel();
AutoCleanScheduler.stopSchedule();
super.dispose();
}
// 初始化缓存
Future<void> _initializeCache() async {
await _huoquDaxiao();
_startAutoCheck();
AutoCleanScheduler.startSchedule();
}
// 启动自动检查
// 每30分钟检查一次缓存大小
void _startAutoCheck() {
_autoCheckTimer?.cancel();
_autoCheckTimer = Timer.periodic(
const Duration(minutes: 30),
(_) => _huoquDaxiao(),
);
}
// 显示错误信息
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: CacheConfig.snackBarDuration,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
// 显示警告信息
void _showWarning() {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.white),
const SizedBox(width: 12),
const Expanded(child: Text('缓存较大,建议清理')),
],
),
backgroundColor: Colors.orange,
duration: CacheConfig.snackBarDuration,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
// 获取缓存大小并检查
Future<void> _huoquDaxiao() async {
try {
final size = await QingLiHuanCun.huoquDaxiao();
if (!mounted) return;
setState(() => _cacheSize = size);
_checkCacheWarning(size);
CacheListener.notifyListeners();
} catch (e) {
_showError('获取缓存大小失败: $e');
}
}
// 检查缓存大小并显示警告
void _checkCacheWarning(String size) {
if (!size.contains('MB')) return;
try {
final double sizeInMB = double.parse(size.split(' ')[0]);
if (sizeInMB > CacheConfig.warningThresholdMB) {
_showWarning();
}
} catch (e) {
// 解析错误处理
}
}
// 显示清理进度
void _showCleaningProgress() {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
const SizedBox(width: 16),
const Text('正在清理缓存...'),
],
),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
// 检查是否是快速双击
bool _checkDoubleClick() {
final now = DateTime.now();
if (_lastClickTime != null) {
final difference = now.difference(_lastClickTime!);
if (difference.inMilliseconds <= 1000) { // 1秒内的双击
_isDoubleClick = true;
return true;
}
}
_lastClickTime = now;
_isDoubleClick = false;
return false;
}
// 修改确认对话框逻辑
Future<bool> _showConfirmDialog() async {
if (_isClearing) return false; // 防止重复清理
// 检查是否是快速双击
final isDoubleClick = _checkDoubleClick();
// 如果不是双击,且缓存小于100MB,显示无需清理提示
if (!isDoubleClick && _cacheSize.contains('MB')) {
try {
final double sizeInMB = double.parse(_cacheSize.split(' ')[0]);
if (sizeInMB < 100.0) {
// 显示缓存较小的提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.info_outline, color: Colors.white),
const SizedBox(width: 12),
const Expanded(
child: Text('缓存小于100MB,暂无需清理\n(快速双击可强制清理)'),
),
],
),
backgroundColor: Colors.blue,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
return false;
}
} catch (e) {
// 解析错误处理
}
}
// 原有的确认对话框逻辑
HapticFeedback.mediumImpact();
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.delete_outline, color: Colors.red),
const SizedBox(width: 12),
Text(_isDoubleClick ? '强制清理' : '确认清理'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('当前缓存大小: $_cacheSize'),
const SizedBox(height: 8),
Text(_isDoubleClick
? '您选择了强制清理,确定要清理所有缓存吗?'
: '清理后将需要重新下载图片,确定要清理吗?'
),
],
),
actions: [
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.pop(context, false);
},
child: const Text('取消'),
),
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.pop(context, true);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: Text(_isDoubleClick ? '强制清理' : '清理'),
),
],
),
);
return confirm ?? false;
}
// 清理缓存
Future<void> _qingliHuancun() async {
final bool confirmed = await _showConfirmDialog();
if (!confirmed) return;
setState(() => _isClearing = true);
try {
_showCleaningProgress();
await QingLiHuanCun.qingliSuoyou();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_outline, color: Colors.white),
const SizedBox(width: 12),
const Text('缓存清理完成'),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
await _huoquDaxiao();
widget.onCleanComplete?.call(true);
}
} catch (e) {
if (mounted) {
_showError('清理失败: $e');
widget.onCleanComplete?.call(false);
}
} finally {
if (mounted) {
setState(() => _isClearing = false);
}
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 显示缓存大小
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'缓存大小',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_cacheSize,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: AnimatedRotation(
duration: CacheConfig.refreshAnimationDuration,
turns: _isClearing ? 1 : 0,
child: const Icon(Icons.refresh),
),
onPressed: _isClearing ? null : () async {
try {
setState(() => _isClearing = true);
HapticFeedback.lightImpact();
// 显示刷新提示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 12),
Text('正在刷新...'),
],
),
duration: Duration(milliseconds: 200),
behavior: SnackBarBehavior.floating,
),
);
await _huoquDaxiao();
} finally {
if (mounted) {
setState(() => _isClearing = false);
}
}
},
),
],
),
],
),
),
),
const SizedBox(height: 16),
// 清理按钮
AnimatedContainer(
duration: CacheConfig.animationDuration,
transform: Matrix4.translationValues(
0, _isClearing ? 4 : 0, 0,
),
child: ElevatedButton(
onPressed: _isClearing ? null : () {
HapticFeedback.mediumImpact();
_qingliHuancun();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isClearing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
'清理缓存',
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 16),
// 自动清理设置
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
),
),
child: ListTile(
leading: const Icon(Icons.auto_delete),
title: const Text('自动清理'),
subtitle: Text(
'当缓存超过${CacheConfig.autoCleanThresholdMB}MB时自动清理'
),
trailing: const Icon(Icons.chevron_right),
onTap: _isClearing ? null : () async {
HapticFeedback.lightImpact();
await QingLiHuanCun.jianchaHeQingli();
await _huoquDaxiao();
},
),
),
],
),
);
}
}
D:\F\luichun\lib\utils\get_images\tupianjiazai.dart
dart
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:typed_data';
import 'huancunguanli.dart';
import '../env_config.dart';
import 'dart:isolate';
import 'logger.dart';
/// 图片加载器类
/// 功能:处理异步图片加载、缓存和显示
/// 工作流程:
/// 1. 接收图片URL请求
/// 2. 检查本地缓存
/// 3. 如无缓存,则在独立isolate中下载
/// 4. 下载完成后保存到缓存
/// 5. 返回图片数据用于显示
class TuPianJiaZai {
static final HuanCunGuanLi _huancun = HuanCunGuanLi();
static bool _initialized = false;
/// 内部初始化方法
/// 确保只初始化一次
static Future<void> _ensureInitialized() async {
if (!_initialized) {
await _huancun.init();
_initialized = true;
ImageLogger.logCacheInfo('图片加载系统初始化完成');
}
}
/// 网络请求客户端配置
/// 功能:配置网络请求的基本参数
/// 参数说明:
/// - connectTimeout: 连接超时时间
/// - receiveTimeout: 接收超时时间
/// - headers: 请求头配置
/// - followRedirects: 是否跟随重定向
/// - maxRedirects: 最大重定向次数
/// - validateStatus: 状态验证函数
static final Dio _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 60),
sendTimeout: const Duration(seconds: 30),
headers: {
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
},
followRedirects: true,
maxRedirects: 5,
validateStatus: (status) => status != null && status < 500,
responseType: ResponseType.bytes,
receiveDataWhenStatusError: true,
));
/// 在独立isolate中加载图片
/// 功能:创建新的isolate来处理图片下载,避免阻塞主线程
/// 参数:
/// url: 图片的网络地址
/// 返回:
/// Uint8List?: 图片的二进制数据,下载失败返回null
/// 工作流程:
/// 1. 创建ReceivePort接收数据
/// 2. 启动新isolate处理下载
/// 3. 等待结果返回
static Future<Uint8List?> _loadInIsolate(String url) async {
final receivePort = ReceivePort();
await Isolate.spawn(_isolateFunction, {
'url': url,
'sendPort': receivePort.sendPort,
});
final result = await receivePort.first;
return result as Uint8List?;
}
/// Isolate工作函数
/// 功能:在独立isolate中执行图片下载
/// 参数:
/// data: 包含url和sendPort的Map
/// 工作流程:
/// 1. 解析传入参数
/// 2. 执行图片下载
/// 3. 通过sendPort返回结果
static void _isolateFunction(Map<String, dynamic> data) async {
final String url = data['url'];
final SendPort sendPort = data['sendPort'];
try {
ImageLogger.logImageDebug('开始下载图片', {'url': url});
int retryCount = 3;
Response<List<int>>? response;
while (retryCount > 0) {
try {
response = await _dio.get<List<int>>(
EnvConfig.getImageUrl(url),
options: Options(
responseType: ResponseType.bytes,
headers: {
'Range': 'bytes=0-',
'Connection': 'keep-alive',
},
),
onReceiveProgress: (received, total) {
if (EnvConfig.isDevelopment) {
ImageLogger.logImageDebug(
'下载进度',
{'received': received, 'total': total}
);
}
},
);
break;
} catch (e) {
retryCount--;
if (retryCount > 0) {
ImageLogger.logImageWarning('图片下载失败,准备重试', {
'url': url,
'remainingRetries': retryCount,
'error': e.toString()
});
await Future.delayed(Duration(seconds: 2));
} else {
rethrow;
}
}
}
if (response != null &&
(response.statusCode == 200 || response.statusCode == 206) &&
response.data != null) {
final imageBytes = Uint8List.fromList(response.data!);
sendPort.send(imageBytes);
} else {
ImageLogger.logImageWarning('图片下载失败', {
'statusCode': response?.statusCode,
'message': response?.statusMessage
});
sendPort.send(null);
}
} catch (e) {
ImageLogger.logImageError('图片下载异常', error: e);
sendPort.send(null);
}
}
/// 加载网络图片的Widget
/// 功能:提供图片加载的Widget封装
/// 参数:
/// imageUrl: 图片URL
/// width: 显示宽度
/// height: 显示高度
/// fit: 图片填充方式
/// placeholder: 加载占位Widget
/// errorWidget: 错误显示Widget
/// cacheWidth: 缓存宽度
/// cacheHeight: 缓存高度
/// 工作流程:
/// 1. 检查URL是否有效
/// 2. 使用FutureBuilder处理异步加载
/// 3. 根据不同状态显示不同Widget
static Widget jiazaiTupian({
required String? imageUrl,
double? width,
double? height,
BoxFit fit = BoxFit.cover,
Widget? placeholder,
Widget? errorWidget,
int? cacheWidth,
int? cacheHeight,
}) {
// 在实际使用时自动初始化
_ensureInitialized();
if (imageUrl == null) {
return const SizedBox.shrink();
}
return FutureBuilder<Uint8List?>(
future: _jiazaiTupianShuju(imageUrl),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return placeholder ?? SizedBox(
width: width,
height: height,
child: const Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError || snapshot.data == null) {
if (EnvConfig.isDevelopment) {
print('图片加载失败: ${snapshot.error}');
print('URL: $imageUrl');
}
return errorWidget ?? SizedBox(
width: width,
height: height,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image),
Text('图片加载失败,请稍后重试'),
],
),
),
);
}
return Image.memory(
snapshot.data!,
key: ValueKey(imageUrl),
width: width,
height: height,
fit: fit,
cacheWidth: cacheWidth ?? (width?.toInt()),
cacheHeight: cacheHeight ?? (height?.toInt()),
gaplessPlayback: true,
);
},
);
}
/// 加载图片数据
/// 功能:处理图片加载的核心逻辑
/// 参数:
/// url: 图片URL
/// 返回:
/// Uint8List?: 图片二进制数据
/// 工作流程:
/// 1. 检查本地缓存
/// 2. 如有缓存直接返回
/// 3. 无缓存则下载并保存
static Future<Uint8List?> _jiazaiTupianShuju(String url) async {
// 在实际使用时自动初始化
await _ensureInitialized();
final huancun = HuanCunGuanLi();
// 先从缓存获取
final cachedImage = await huancun.huoquTupian(url);
if (cachedImage != null) {
if (EnvConfig.isDevelopment) {
print('从缓存加载图片: $url');
}
return cachedImage;
}
// 在独立isolate中加载图片
final imageBytes = await _loadInIsolate(url);
if (imageBytes != null) {
// 保存到缓存
await huancun.baocunTupian(url, imageBytes);
}
return imageBytes;
}
}