导语
原生 Image.network 存在加载无占位、错误无兜底、无缓存、样式单一等痛点,不仅影响用户体验,还会造成不必要的流量浪费。本文基于 cached_network_image 封装通用网络图片组件 CachedImageWidget,集成占位加载、错误重试、本地缓存、圆角 / 圆形裁剪等核心功能,通过精细化优化适配所有图片展示场景,实现 "一次封装,全域复用"!
一、核心需求升级拆解
在基础需求上强化实用性与体验感,覆盖更多业务场景:✅ 加载中支持骨架屏 / 自定义占位图,视觉过渡更自然✅ 加载失败显示兜底图,支持点击重试,提升容错性✅ 本地缓存自动管理,可配置缓存时长与最大缓存数量✅ 支持圆角、圆形、边框样式,无需额外嵌套组件✅ 加载完成渐变动画,优化视觉体验✅ 适配暗黑模式,样式统一协调✅ 支持图片预加载,提前缓存高频访问图片
二、依赖准备(稳定版)
yaml
dependencies:
flutter:
sdk: flutter
cached_network_image: ^3.3.1 # 图片缓存核心依赖(支持缓存、占位、错误处理)
flutter_cache_manager: ^3.3.1 # 缓存管理(自定义缓存策略)
执行命令安装依赖:
bash
运行
flutter pub get
三、完整代码实现(带优化注释)
dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'skeleton_widget.dart'; // 引入之前封装的骨架屏组件(无则可替换为默认占位)
/// 通用网络图片组件(支持缓存、占位、错误重试、样式自定义)
class CachedImageWidget extends StatefulWidget {
// 必选参数
final String imageUrl; // 图片网络地址(支持http/https)
final double width; // 图片宽度(必填,避免布局抖动)
final double height; // 图片高度(必填,避免布局抖动)
// 可选参数(带合理默认值,降低使用成本)
final BoxFit fit; // 图片缩放模式(默认cover,保持比例填充)
final double borderRadius; // 圆角半径(默认0,不圆角)
final bool isCircle; // 是否圆形裁剪(优先级高于圆角,默认false)
final Widget? placeholder; // 自定义占位图(优先级高于骨架屏)
final Widget? errorWidget; // 自定义错误图(优先级高于默认错误图)
final Duration fadeDuration; // 加载完成渐变动画时长(默认300ms)
final bool enableRetry; // 是否允许点击重试(默认true)
final int cacheDurationDays; // 缓存有效期(默认7天)
final int maxCacheCount; // 最大缓存数量(默认100张,避免内存占用过多)
final Color? borderColor; // 图片边框颜色(默认无边框)
final double borderWidth; // 边框宽度(默认0,无边框)
final bool preload; // 是否预加载图片(默认false,需主动触发时设为true)
const CachedImageWidget({
super.key,
required this.imageUrl,
required this.width,
required this.height,
this.fit = BoxFit.cover,
this.borderRadius = 0.0,
this.isCircle = false,
this.placeholder,
this.errorWidget,
this.fadeDuration = const Duration(milliseconds: 300),
this.enableRetry = true,
this.cacheDurationDays = 7,
this.maxCacheCount = 100,
this.borderColor,
this.borderWidth = 0.0,
this.preload = false,
});
@override
State<CachedImageWidget> createState() => _CachedImageWidgetState();
}
class _CachedImageWidgetState extends State<CachedImageWidget> {
bool _isLoading = true; // 加载中状态
bool _isError = false; // 加载错误状态
late CacheManager _cacheManager; // 自定义缓存管理器
@override
void initState() {
super.initState();
// 初始化缓存管理器
_cacheManager = CacheManager(
Config(
'flutter_cached_images', // 缓存目录名称
stalePeriod: Duration(days: widget.cacheDurationDays), // 缓存有效期
maxNrOfCacheObjects: widget.maxCacheCount, // 最大缓存数量
repo: JsonCacheInfoRepository(databaseName: 'cached_images.db'),
fileService: HttpFileService(),
),
);
// 预加载图片(仅当preload为true时触发)
if (widget.preload) {
_preloadImage();
}
}
@override
void dispose() {
// 释放缓存管理器资源
_cacheManager.dispose();
super.dispose();
}
/// 预加载图片到缓存
Future<void> _preloadImage() async {
try {
await _cacheManager.downloadFile(widget.imageUrl);
if (mounted) {
setState(() => _isLoading = false);
}
} catch (e) {
if (mounted) {
setState(() => _isError = true);
}
}
}
/// 重新加载图片
void _reloadImage() {
if (_isError && mounted) {
setState(() {
_isError = false;
_isLoading = true;
});
// 清除该图片的缓存后重新加载
_cacheManager.removeFile(widget.imageUrl).then((_) {
if (mounted) {
setState(() {}); // 触发重建,重新加载图片
}
});
}
}
/// 构建基础裁剪与边框样式
Widget _buildImageContainer(Widget child) {
// 边框配置
final BoxBorder? border = widget.borderWidth > 0 && widget.borderColor != null
? Border.all(
color: widget.borderColor!,
width: widget.borderWidth,
)
: null;
// 圆形/圆角裁剪 + 边框
return ClipRRect(
borderRadius: widget.isCircle
? BorderRadius.circular(widget.height / 2) // 圆形:半径=高度/2
: BorderRadius.circular(widget.borderRadius),
child: Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
border: border,
borderRadius: widget.isCircle
? BorderRadius.circular(widget.height / 2)
: BorderRadius.circular(widget.borderRadius),
),
child: child,
),
);
}
/// 构建默认占位图(骨架屏)
Widget _buildDefaultPlaceholder() {
return SkeletonWidget(
width: widget.width,
height: widget.height,
isCircle: widget.isCircle,
borderRadius: widget.borderRadius,
);
}
/// 构建默认错误图
Widget _buildDefaultErrorWidget() {
return Container(
width: widget.width,
height: widget.height,
color: Colors.grey.shade100,
alignment: Alignment.center,
child: const Icon(
Icons.broken_image_outlined,
color: Colors.grey,
size: 32,
),
);
}
@override
Widget build(BuildContext context) {
// 基础图片组件(缓存+加载状态管理)
final Widget imageContent = CachedNetworkImage(
imageUrl: widget.imageUrl,
width: widget.width,
height: widget.height,
fit: widget.fit,
fadeInDuration: widget.fadeDuration,
fadeOutDuration: const Duration(milliseconds: 200),
cacheManager: _cacheManager,
// 加载中占位图
placeholder: (context, url) => widget.placeholder ?? _buildDefaultPlaceholder(),
// 加载错误处理
errorWidget: (context, url, error) {
if (mounted) {
setState(() {
_isLoading = false;
_isError = true;
});
}
// 错误图 + 点击重试
final errorContent = widget.errorWidget ?? _buildDefaultErrorWidget();
return widget.enableRetry
? GestureDetector(
onTap: _reloadImage,
behavior: HitTestBehavior.opaque,
child: errorContent,
)
: errorContent;
},
// 加载完成回调
imageBuilder: (context, imageProvider) {
if (mounted) {
setState(() {
_isLoading = false;
_isError = false;
});
}
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: widget.fit,
),
),
);
},
);
// 组合裁剪、边框、图片内容
return _buildImageContainer(imageContent);
}
}
四、丰富使用示例(覆盖全场景)
dart
class CachedImageDemo extends StatelessWidget {
const CachedImageDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('通用网络图片示例')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('1. 圆形头像(带骨架占位+边框)'),
const SizedBox(height: 10),
// 圆形头像(带边框+骨架占位)
CachedImageWidget(
imageUrl: 'https://example.com/avatar.png',
width: 60,
height: 60,
isCircle: true,
borderColor: Colors.blue,
borderWidth: 2,
cacheDurationDays: 30, // 头像缓存30天
),
const SizedBox(height: 20),
const Text('2. 矩形图片(自定义错误图+圆角)'),
const SizedBox(height: 10),
// 矩形图片(自定义错误图+圆角)
CachedImageWidget(
imageUrl: 'https://example.com/banner.png',
width: double.infinity,
height: 200,
borderRadius: 12,
fit: BoxFit.cover,
errorWidget: Container(
color: Colors.grey.shade100,
alignment: Alignment.center,
child: const Text(
'图片加载失败\n点击重试',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
enableRetry: true,
),
const SizedBox(height: 20),
const Text('3. 列表项图片(预加载+小圆角)'),
const SizedBox(height: 10),
// 列表项图片(预加载+小圆角)
CachedImageWidget(
imageUrl: 'https://example.com/item.jpg',
width: 80,
height: 80,
borderRadius: 8,
fit: BoxFit.cover,
preload: true, // 预加载,提升列表滑动体验
maxCacheCount: 50, // 列表图片缓存上限50张
),
const SizedBox(height: 20),
const Text('4. 暗黑模式适配图片(边框+渐变动画)'),
const SizedBox(height: 10),
// 暗黑模式适配(边框+渐变动画)
CachedImageWidget(
imageUrl: 'https://example.com/dark-mode.jpg',
width: 150,
height: 150,
borderRadius: 8,
borderColor: Theme.of(context).primaryColor,
borderWidth: 1,
fadeDuration: const Duration(milliseconds: 500), // 延长渐变动画
),
],
),
),
);
}
}
五、优化亮点与核心技巧
1. 功能增强(解决原生痛点)
- 缓存精细化管理:支持配置缓存时长、最大缓存数量,避免无限制占用存储空间;自定义缓存目录与数据库名称,便于维护。
- 预加载功能 :新增
preload参数,列表、轮播图等场景可提前缓存图片,提升滑动流畅度。 - 边框样式支持 :无需额外嵌套
Container,直接通过borderColor和borderWidth配置边框,简化代码。 - 错误重试优化:重试时先清除该图片的旧缓存,避免因缓存损坏导致的重复失败。
2. 体验与适配优化
- 布局稳定性 :强制要求传入
width和height,避免图片加载过程中布局抖动(原生Image.network常见问题)。 - 视觉过渡自然:加载完成渐变动画、骨架屏占位,替代生硬的瞬间显示,提升用户体验。
- 暗黑模式适配 :边框颜色支持通过
Theme.of(context)获取,自动适配亮色 / 暗黑模式,样式统一。 - 错误提示友好:默认错误图使用轮廓图标,视觉更轻盈;支持自定义错误文本,信息传达更清晰。
3. 稳定性与性能保障
- 资源释放 :在
dispose中释放CacheManager资源,避免内存泄漏。 - 状态安全 :所有状态更新前判断
mounted,防止组件销毁后触发重建导致崩溃。 - 缓存容错:预加载失败时自动切换为错误状态,不影响页面正常展示。
六、避坑指南(开发者必看)
| 常见问题 | 表现形式 | 解决方案 |
|---|---|---|
| 图片加载无占位,布局抖动 | 图片未加载完成时显示空白,加载后布局偏移 | 确保传入固定width和height;使用骨架屏占位,避免依赖图片自身尺寸 |
| 缓存不生效,重复请求 | 每次打开页面都重新加载图片 | 检查cacheDurationDays是否设置合理;确认网络图片 URL 是否固定(动态 URL 会重新缓存) |
| 圆形裁剪变形 | 圆形图片显示为椭圆 | 确保width和height相等;isCircle设为true时,圆角配置会失效,无需重复设置 |
| 点击重试无响应 | 错误状态下点击图片不触发重试 | 检查enableRetry是否为true;自定义errorWidget需确保GestureDetector的behavior设为HitTestBehavior.opaque |
| 缓存占用过多 | 设备存储空间逐渐增大 | 合理设置maxCacheCount(建议列表类图片设为 50-100 张);定期清理过期缓存 |
总结
优化后的 CachedImageWidget 不仅解决了原生 Image.network 的核心痛点,还通过缓存精细化、样式自定义、体验优化等增强,适配了头像、列表项、轮播图、横幅等所有图片展示场景。其核心优势在于 "通用性强、配置灵活、体验优秀、稳定性高",是 Flutter 项目中图片展示的必备组件。