flutter 图片加载类 图片的安全使用 建议使用方法2
版本1
1.1使用
dart
复制代码
void main() {
WidgetsFlutterBinding.ensureInitialized();
// 关键:初始化全局配置
KimiImageConfig.init(
stalePeriod: const Duration(days: 3), // 磁盘缓存3天
maxCacheObjects: 100, // 最多100个文件
);
runApp(MyApp());
}
1.2 依赖
dart
复制代码
dependencies:
flutter:
sdk: flutter
# 缓存
cached_network_image: ^3.3.0
flutter_cache_manager: ^3.3.1
#加载状态
shimmer: ^3.0.0
visibility_detector: ^0.4.0+2 # 懒加载
crypto: ^3.0.3 # 缓存Key生成
1.3工具类
dart
复制代码
import 'dart:io';
import 'dart:typed_data';
import 'dart:collection';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:shimmer/shimmer.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
/**
* KimiImage - 生产级图片组件(修复版)
* 修复问题:
* 1. 磁盘缓存大小限制(自定义CacheManager)
* 2. 移除无效的maxAge参数
* 3. 缓存Key生成优化(避免冲突)
* 4. 异步文件检查(避免UI阻塞)
* 5. 自动重试机制(指数退避)
* 6. 支持暗黑模式
* 7. 加载回调
*/
// ==================== 全局配置 ====================
class KimiImageConfig {
static bool _initialized = false;
static late final CacheManager cacheManager;
static void init({
Duration stalePeriod = const Duration(days: 3),
int maxCacheObjects = 100,
}) {
if (_initialized) return;
cacheManager = CacheManager(
Config(
'kimi_image_cache',
stalePeriod: stalePeriod,
maxNrOfCacheObjects: maxCacheObjects,
fileService: HttpFileService(),
),
);
PaintingBinding.instance.imageCache
..maximumSize = 100
..maximumSizeBytes = 100 * 1024 * 1024;
_initialized = true;
debugPrint('✅ KimiImage初始化完成');
}
static Future<void> clearAll() async {
await cacheManager.emptyCache();
_CompressionCache().clear();
PaintingBinding.instance.imageCache.clear();
}
}
// ==================== 内存缓存(LRU + 并发控制)====================
class _CompressionCache {
static final _CompressionCache _instance = _CompressionCache._internal();
factory _CompressionCache() => _instance;
_CompressionCache._internal();
final LinkedHashMap<String, _CacheEntry> _cache = LinkedHashMap();
final Map<String, Future<Uint8List?>> _loading = {};
static const int _maxSize = 50;
static const Duration _maxAge = Duration(minutes: 10);
Uint8List? get(String key) {
final entry = _cache.remove(key);
if (entry == null) return null;
if (DateTime.now().difference(entry.time) > _maxAge) return null;
_cache[key] = entry;
return entry.data;
}
void set(String key, Uint8List data) {
while (_cache.length >= _maxSize) {
_cache.remove(_cache.keys.first);
}
_cache[key] = _CacheEntry(data: data, time: DateTime.now());
}
Future<Uint8List?> load(String key, Future<Uint8List?> Function() loader) async {
final cached = get(key);
if (cached != null) return cached;
if (_loading.containsKey(key)) return _loading[key]!;
final future = loader().then((data) {
if (data != null) set(key, data);
_loading.remove(key);
return data;
}).catchError((e) {
_loading.remove(key);
throw e;
});
_loading[key] = future;
return future;
}
void clear() {
_cache.clear();
_loading.clear();
}
}
class _CacheEntry {
final Uint8List data;
final DateTime time;
_CacheEntry({required this.data, required this.time});
}
// ==================== 核心组件 ====================
class KimiImage extends StatefulWidget {
final String image;
final double width;
final double height;
final BoxFit fit;
final BorderRadius? borderRadius;
final bool circle;
final bool lazy;
final Duration fadeDuration;
final VoidCallback? onTap;
final VoidCallback? onLoad; // 新增:加载成功回调
final VoidCallback? onError; // 新增:加载失败回调
final Map<String, String>? headers;
final int quality;
final int maxDimension;
const KimiImage(
this.image, {
super.key,
this.width = 100,
this.height = 100,
this.fit = BoxFit.cover,
this.borderRadius,
this.circle = false,
this.lazy = true,
this.fadeDuration = const Duration(milliseconds: 300),
this.onTap,
this.onLoad,
this.onError,
this.headers,
this.quality = 85,
this.maxDimension = 1080,
});
@override
State<KimiImage> createState() => _KimiImageState();
}
class _KimiImageState extends State<KimiImage> {
bool _shouldLoad = false;
bool _disposed = false;
int _retryCount = 0; // 新增:重试计数
late final String _cacheKey;
late final String _uniqueId;
@override
void initState() {
super.initState();
_uniqueId = '${widget.image.hashCode}_${hashCode}_${DateTime.now().microsecond}';
_cacheKey = _generateCacheKey();
if (!widget.lazy) _shouldLoad = true;
}
// 修复:明确的字符串拼接,避免类型隐式转换问题
String _generateCacheKey() {
final data = '${widget.image}_${widget.maxDimension}_${widget.quality}';
return md5.convert(utf8.encode(data)).toString();
}
// 修复:异步检查文件,避免UI线程阻塞
Future<ImageSourceType> _getSourceType() async {
if (widget.image.startsWith('http')) return ImageSourceType.network;
if (widget.image.startsWith('assets')) return ImageSourceType.asset;
try {
if (await File(widget.image).exists()) return ImageSourceType.file;
} catch (e) {
debugPrint('文件检查失败: $e');
}
return ImageSourceType.unknown;
}
void _safeSetState(VoidCallback fn) {
if (mounted && !_disposed) setState(fn);
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget content = widget.lazy && !_shouldLoad
? _buildLazyWrapper()
: _buildContent();
if (widget.circle) {
content = ClipOval(child: content);
} else if (widget.borderRadius != null) {
content = ClipRRect(borderRadius: widget.borderRadius!, child: content);
}
if (widget.onTap != null) {
content = GestureDetector(
onTap: widget.onTap,
behavior: HitTestBehavior.opaque,
child: content,
);
}
return content;
}
Widget _buildLazyWrapper() {
return VisibilityDetector(
key: Key('lazy_$_uniqueId'),
onVisibilityChanged: (info) {
if (info.visibleFraction > 0.05 && !_shouldLoad) {
_safeSetState(() => _shouldLoad = true);
}
},
child: _buildPlaceholder(),
);
}
// 修复:使用FutureBuilder处理异步sourceType检查
Widget _buildContent() {
return FutureBuilder<ImageSourceType>(
future: _getSourceType(),
builder: (context, snapshot) {
if (!snapshot.hasData) return _buildPlaceholder();
switch (snapshot.data!) {
case ImageSourceType.network:
return _buildNetworkImage();
case ImageSourceType.asset:
return _buildAssetImage();
case ImageSourceType.file:
return _buildFileImage();
case ImageSourceType.unknown:
return _buildError('无效路径');
}
},
);
}
/// 网络图片
Widget _buildNetworkImage() {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final memWidth = (widget.width * pixelRatio * 1.5).toInt();
final memHeight = (widget.height * pixelRatio * 1.5).toInt();
return CachedNetworkImage(
imageUrl: widget.image,
cacheManager: KimiImageConfig.cacheManager,
width: widget.width,
height: widget.height,
fit: widget.fit,
cacheKey: widget.image,
memCacheWidth: memWidth,
memCacheHeight: memHeight,
// 修复:移除无效的maxAge参数
httpHeaders: widget.headers,
fadeInDuration: widget.fadeDuration,
placeholder: (_, __) => _buildPlaceholder(),
errorWidget: (_, __, ___) => _buildRetryWidget(), // 修复:使用重试组件
);
}
/// 修复:自动重试组件(指数退避)
Widget _buildRetryWidget() {
// 自动重试3次
if (_retryCount < 3) {
Future.delayed(Duration(seconds: 1 << _retryCount), () {
if (mounted) {
_safeSetState(() => _retryCount++);
}
});
return _buildPlaceholder();
}
// 超过3次显示错误
widget.onError?.call();
return GestureDetector(
onTap: () => _safeSetState(() => _retryCount = 0),
child: Container(
width: widget.width,
height: widget.height,
color: Colors.grey[100],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.wifi_off, color: Colors.grey[400], size: 32),
Text('加载失败', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
Text('点击重试', style: TextStyle(color: Colors.grey[400], fontSize: 10)),
],
),
),
);
}
Widget _buildAssetImage() {
return FutureBuilder<Uint8List?>(
future: _CompressionCache().load(_cacheKey, _loadAsset),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildPlaceholder();
}
if (snapshot.hasError || snapshot.data == null) {
return _buildError('加载失败');
}
widget.onLoad?.call(); // 回调
return _buildMemoryImage(snapshot.data!);
},
);
}
Future<Uint8List?> _loadAsset() async {
final data = await rootBundle.load(widget.image);
return _compress(data.buffer.asUint8List());
}
Widget _buildFileImage() {
return FutureBuilder<Uint8List?>(
future: _CompressionCache().load(_cacheKey, _loadFile),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildPlaceholder();
}
if (snapshot.hasError || snapshot.data == null) {
return _buildError('文件不存在');
}
widget.onLoad?.call();
return _buildMemoryImage(snapshot.data!);
},
);
}
Future<Uint8List?> _loadFile() async {
final file = File(widget.image);
if (!await file.exists()) return null;
return _compress(await file.readAsBytes());
}
Future<Uint8List?> _compress(Uint8List bytes) async {
if (bytes.length < 50 * 1024) return bytes;
try {
return await FlutterImageCompress.compressWithList(
bytes,
quality: widget.quality,
minWidth: widget.maxDimension,
minHeight: widget.maxDimension,
format: CompressFormat.webp,
autoCorrectionAngle: true,
);
} catch (e) {
debugPrint('压缩失败: $e');
return bytes;
}
}
Widget _buildMemoryImage(Uint8List bytes) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: widget.fadeDuration,
builder: (context, value, child) => Opacity(opacity: value, child: child),
child: Image.memory(
bytes,
width: widget.width,
height: widget.height,
fit: widget.fit,
gaplessPlayback: true,
),
);
}
/// 修复:支持暗黑模式的Shimmer
Widget _buildPlaceholder() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Shimmer.fromColors(
baseColor: isDark ? Colors.grey[800]! : Colors.grey[300]!,
highlightColor: isDark ? Colors.grey[700]! : Colors.grey[100]!,
child: Container(
width: widget.width,
height: widget.height,
color: isDark ? Colors.black : Colors.white,
),
);
}
Widget _buildError(String msg) {
widget.onError?.call();
return GestureDetector(
onTap: () => _safeSetState(() {}),
child: Container(
width: widget.width,
height: widget.height,
color: Colors.grey[100],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, color: Colors.grey[400], size: 32),
Text(msg, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
],
),
),
);
}
}
enum ImageSourceType { network, asset, file, unknown }
// ==================== 便捷组件 ====================
class KimiImageList extends StatelessWidget {
final List<String> images;
final Axis direction;
final double itemWidth;
final double itemHeight;
final double spacing;
final EdgeInsets padding;
final Function(int index, String url)? onTap;
const KimiImageList({
super.key,
required this.images,
this.direction = Axis.horizontal,
this.itemWidth = 100,
this.itemHeight = 100,
this.spacing = 8,
this.padding = const EdgeInsets.all(16),
this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: direction == Axis.horizontal ? itemHeight : null,
child: ListView.separated(
scrollDirection: direction,
padding: padding,
itemCount: images.length,
cacheExtent: 200,
addAutomaticKeepAlives: false,
separatorBuilder: (_, __) => SizedBox(
width: direction == Axis.horizontal ? spacing : 0,
height: direction == Axis.vertical ? spacing : 0,
),
itemBuilder: (context, index) => KimiImage(
images[index],
width: itemWidth,
height: itemHeight,
maxDimension: 400,
quality: 75,
lazy: true,
onTap: () => onTap?.call(index, images[index]),
),
),
);
}
}
class KimiAvatar extends StatelessWidget {
final String url;
final double size;
final VoidCallback? onTap;
const KimiAvatar(this.url, {super.key, this.size = 48, this.onTap});
@override
Widget build(BuildContext context) {
return KimiImage(
url,
width: size,
height: size,
circle: true,
maxDimension: 128,
quality: 80,
lazy: false,
onTap: onTap,
);
}
}
2 优化版
2.1 pubspec.yaml 必须添加的依赖
dart
复制代码
dependencies:
flutter:
sdk: flutter
cached_network_image: ^3.3.0
flutter_image_compress: ^2.1.0
visibility_detector: ^0.4.0+2
shimmer: ^3.0.0
crypto: ^3.0.3
flutter_cache_manager: ^3.3.1
2.2 使用方式
dart
复制代码
// 1. main.dart 初始化
void main() {
WidgetsFlutterBinding.ensureInitialized();
ImageConfig.init(); // 必须调用
runApp(MyApp());
}
// 2. 基础使用
OptimizedImage(
'https://xxx.jpg',
width: 100,
height: 100,
circle: true,
onLoad: () {},
onError: () {},
)
// 3. 清理缓存
ImageConfig.clearCache();
2.3 代码
dart
复制代码
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:shimmer/shimmer.dart';
import 'package:crypto/crypto.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
// ==================== 配置 ====================
class ImageConfig {
static bool _initialized = false;
static late final CacheManager _cacheManager;
static CacheManager get cacheManager {
if (!_initialized) {
// 自动初始化而非抛异常
init();
}
return _cacheManager;
}
static void init({
Duration stalePeriod = const Duration(days: 7),
int maxCacheObjects = 100,
}) {
if (_initialized) return;
_cacheManager = CacheManager(
Config(
'app_image_cache',
stalePeriod: stalePeriod,
maxNrOfCacheObjects: maxCacheObjects,
fileService: HttpFileService(),
),
);
PaintingBinding.instance.imageCache
..maximumSize = 100
..maximumSizeBytes = 100 * 1024 * 1024;
_initialized = true;
}
static Future<void> clearCache() async {
if (!_initialized) return;
await _cacheManager.emptyCache();
_ImageCache().clear();
PaintingBinding.instance.imageCache.clear();
}
}
// ==================== 内存缓存(豆包方案优化)====================
class _ImageCache {
static final _ImageCache _instance = _ImageCache._internal();
factory _ImageCache() => _instance;
_ImageCache._internal();
final LinkedHashMap<String, _Entry> _cache = LinkedHashMap();
final Map<String, Future<Uint8List?>> _loading = {};
static const int _maxCount = 50;
static const Duration _maxAge = Duration(minutes: 10);
int _currentMemoryBytes = 0;
static const int _maxMemoryBytes = 50 * 1024 * 1024; // 提高到50MB
Uint8List? get(String key) {
final entry = _cache.remove(key);
if (entry == null) return null;
if (DateTime.now().difference(entry.time) > _maxAge) {
_currentMemoryBytes -= entry.data.length;
return null;
}
_cache[key] = entry;
return entry.data;
}
void set(String key, Uint8List data) {
final old = _cache.remove(key);
if (old != null) _currentMemoryBytes -= old.data.length;
// 清理直到满足限制
while (_cache.length >= _maxCount ||
(_currentMemoryBytes + data.length > _maxMemoryBytes && _cache.isNotEmpty)) {
final oldest = _cache.remove(_cache.keys.first)!;
_currentMemoryBytes -= oldest.data.length;
}
_cache[key] = _Entry(data: data, time: DateTime.now());
_currentMemoryBytes += data.length;
}
Future<Uint8List?> load(
String key,
Future<Uint8List?> Function() loader, {
CancelToken? cancelToken,
}) async {
final cached = get(key);
if (cached != null) return cached;
if (_loading.containsKey(key)) return _loading[key]!;
if (cancelToken?.isCancelled ?? false) return null;
final future = loader().then((data) {
if (data != null && !(cancelToken?.isCancelled ?? false)) set(key, data);
_loading.remove(key);
return data;
}).catchError((e) {
_loading.remove(key);
throw e;
});
_loading[key] = future;
return future;
}
void clear() {
_cache.clear();
_loading.clear();
_currentMemoryBytes = 0;
}
}
class _Entry {
final Uint8List data;
final DateTime time;
_Entry({required this.data, required this.time});
}
class CancelToken {
bool isCancelled = false;
void cancel() => isCancelled = true;
}
// ==================== 核心组件(关键修复)====================
class OptimizedImage extends StatefulWidget {
final String image;
final double? width;
final double? height;
final BoxFit fit;
final BorderRadius? borderRadius;
final bool circle;
final bool lazy;
final Duration fadeDuration;
final VoidCallback? onTap;
final VoidCallback? onLoad;
final VoidCallback? onError;
final Map<String, String>? headers;
final int quality;
final int maxDimension;
final Widget? placeholder;
final Widget? errorWidget;
const OptimizedImage(
this.image, {
super.key,
this.width,
this.height,
this.fit = BoxFit.cover,
this.borderRadius,
this.circle = false,
this.lazy = true,
this.fadeDuration = const Duration(milliseconds: 300),
this.onTap,
this.onLoad,
this.onError,
this.headers,
this.quality = 85,
this.maxDimension = 1080,
this.placeholder,
this.errorWidget,
});
@override
State<OptimizedImage> createState() => _OptimizedImageState();
}
class _OptimizedImageState extends State<OptimizedImage> {
bool _shouldLoad = false;
late final String _cacheKey;
late final CancelToken _cancelToken;
int _retryCount = 0;
static const int _maxRetry = 3;
@override
void initState() {
super.initState();
_cancelToken = CancelToken();
_cacheKey = _generateCacheKey();
if (!widget.lazy) _shouldLoad = true;
}
@override
void dispose() {
_cancelToken.cancel();
super.dispose();
}
String _generateCacheKey() {
String url = widget.image;
if (url.contains('?')) url = url.split('?').first;
return md5.convert(utf8.encode('${url}_${widget.maxDimension}_${widget.quality}')).toString();
}
void _safeSetState(VoidCallback fn) {
if (mounted) setState(fn);
}
@override
Widget build(BuildContext context) {
Widget content = widget.lazy && !_shouldLoad ? _buildLazyWrapper() : _buildContent();
if (widget.circle) {
content = ClipOval(child: content);
} else if (widget.borderRadius != null) {
content = ClipRRect(borderRadius: widget.borderRadius!, child: content);
}
if (widget.onTap != null) {
content = GestureDetector(onTap: widget.onTap, behavior: HitTestBehavior.opaque, child: content);
}
// 关键修复:正确处理null尺寸
if (widget.width != null && widget.height != null) {
return SizedBox(width: widget.width, height: widget.height, child: content);
}
return content;
}
Widget _buildLazyWrapper() {
return VisibilityDetector(
key: Key('lazy_${widget.image}_$hashCode'),
onVisibilityChanged: (info) {
if (info.visibleFraction > 0.05 && !_shouldLoad) {
_safeSetState(() => _shouldLoad = true);
}
},
child: _buildPlaceholder(),
);
}
// ==================== 关键修复:区分网络与本地图片 ====================
Widget _buildContent() {
// 网络图片:用CachedNetworkImage(最优性能)
if (widget.image.startsWith('http')) {
return _buildNetworkImage();
}
// 本地图片:走压缩缓存
return FutureBuilder<ImageSourceType>(
future: _getLocalSourceType(),
builder: (context, snapshot) {
if (!snapshot.hasData) return _buildPlaceholder();
if (snapshot.data == ImageSourceType.unknown) return _buildError('无效路径');
return _buildLocalImage();
},
);
}
/// 关键修复:网络图片用CachedNetworkImage,渐进加载+内存限制
Widget _buildNetworkImage() {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final memWidth = widget.width != null ? (widget.width! * pixelRatio).toInt() : null;
final memHeight = widget.height != null ? (widget.height! * pixelRatio).toInt() : null;
return CachedNetworkImage(
imageUrl: widget.image,
cacheManager: ImageConfig.cacheManager,
width: widget.width,
height: widget.height,
fit: widget.fit,
cacheKey: _getBaseUrl(widget.image),
memCacheWidth: memWidth, // 关键:限制解码内存
memCacheHeight: memHeight,
httpHeaders: widget.headers,
fadeInDuration: widget.fadeDuration,
placeholder: (_, __) => _buildPlaceholder(),
errorWidget: (_, __, ___) {
// 自动重试逻辑
if (_retryCount < _maxRetry) {
_scheduleRetry();
return _buildPlaceholder();
}
widget.onError?.call();
return _buildErrorWidget();
},
);
}
/// 本地图片:压缩后缓存
Widget _buildLocalImage() {
return FutureBuilder<Uint8List?>(
future: _ImageCache().load(_cacheKey, _loadLocalImage, cancelToken: _cancelToken),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildPlaceholder();
}
if (snapshot.hasError || snapshot.data == null) {
if (_retryCount < _maxRetry) {
_scheduleRetry();
return _buildPlaceholder();
}
widget.onError?.call();
return _buildError('加载失败');
}
widget.onLoad?.call();
return _buildMemoryImage(snapshot.data!);
},
);
}
Future<Uint8List?> _loadLocalImage() async {
Uint8List? bytes;
if (widget.image.startsWith('assets')) {
final data = await rootBundle.load(widget.image);
bytes = data.buffer.asUint8List();
} else {
final file = File(widget.image);
if (await file.exists()) {
bytes = await file.readAsBytes();
}
}
if (bytes == null) return null;
return _compress(bytes);
}
Future<ImageSourceType> _getLocalSourceType() async {
if (widget.image.startsWith('assets')) return ImageSourceType.asset;
try {
if (await File(widget.image).exists()) return ImageSourceType.file;
} catch (_) {}
return ImageSourceType.unknown;
}
String _getBaseUrl(String url) {
return url.contains('?') ? url.substring(0, url.indexOf('?')) : url;
}
void _scheduleRetry() {
final delay = Duration(seconds: 1 << _retryCount);
Future.delayed(delay, () {
if (mounted) {
_retryCount++;
_safeSetState(() {});
}
});
}
Future<Uint8List?> _compress(Uint8List bytes) async {
if (bytes.length < 50 * 1024) return bytes;
try {
return await FlutterImageCompress.compressWithList(
bytes,
quality: widget.quality,
minWidth: widget.maxDimension,
minHeight: widget.maxDimension,
format: CompressFormat.webp,
);
} catch (e) {
return bytes;
}
}
Widget _buildMemoryImage(Uint8List bytes) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: widget.fadeDuration,
builder: (_, value, child) => Opacity(opacity: value, child: child),
child: Image.memory(
bytes,
fit: widget.fit,
gaplessPlayback: true,
width: widget.width,
height: widget.height,
),
);
}
Widget _buildPlaceholder() {
if (widget.placeholder != null) return widget.placeholder!;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Shimmer.fromColors(
baseColor: isDark ? Colors.grey[800]! : Colors.grey[300]!,
highlightColor: isDark ? Colors.grey[700]! : Colors.grey[100]!,
child: Container(color: isDark ? Colors.black : Colors.white),
);
}
Widget _buildError(String msg) {
if (widget.errorWidget != null) return widget.errorWidget!;
return GestureDetector(
onTap: () => _safeSetState(() => _retryCount = 0),
child: Container(
color: Colors.grey[100],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, color: Colors.grey[400]),
Text(msg, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
],
),
),
);
}
Widget _buildErrorWidget() => _buildError('加载失败');
}
enum ImageSourceType { network, asset, file, unknown }
// ==================== 列表组件 ====================
class OptimizedImageList extends StatelessWidget {
final List<String> images;
final Axis direction;
final double itemWidth;
final double itemHeight;
final double spacing;
final EdgeInsets padding;
final Function(int index, String url)? onTap;
const OptimizedImageList({
super.key,
required this.images,
this.direction = Axis.horizontal,
this.itemWidth = 100,
this.itemHeight = 100,
this.spacing = 8,
this.padding = const EdgeInsets.all(16),
this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: direction == Axis.horizontal ? itemHeight : null,
child: ListView.separated(
scrollDirection: direction,
padding: padding,
itemCount: images.length,
cacheExtent: 200,
addAutomaticKeepAlives: false,
separatorBuilder: (_, __) => SizedBox(
width: direction == Axis.horizontal ? spacing : 0,
height: direction == Axis.vertical ? spacing : 0,
),
itemBuilder: (context, index) => OptimizedImage(
images[index],
width: itemWidth,
height: itemHeight,
maxDimension: 400,
quality: 75,
lazy: true,
onTap: () => onTap?.call(index, images[index]),
),
),
);
}
}