在 Flutter 开发中,网络图片加载是高频场景,但原生Image.network存在诸多短板:加载时空白无反馈、失败后无兜底样式、不支持灵活裁剪、缺乏过渡动画,直接使用不仅导致 UI 体验割裂,还会产生大量重复代码。本文封装的ImageLoaderWidget,整合 "加载占位 + 失败重试 + 形态裁剪 + 过渡动画" 四大核心能力,一行代码即可搞定网络图片加载,适配 90%+ 场景!
一、核心需求拆解
✅ 加载中占位:支持默认骨架屏、加载指示器,也可自定义占位组件✅ 失败兜底与重试:加载失败显示友好图标,支持点击重试,提升用户体验✅ 多形态裁剪:支持圆形(头像)、圆角(商品图)、方形(普通图)三种形态✅ 过渡动画:内置淡入动画,避免图片加载突兀,视觉更流畅✅ 全参数自定义:尺寸、适配模式、占位色、动画时长均可配置,适配不同设计风格
二、完整代码实现(可直接复制使用)
dart
import 'package:flutter/material.dart';
// 图片裁剪形态枚举(覆盖高频使用场景)
enum ImageShape {
circle, // 圆形(适配用户头像、图标)
rounded, // 圆角方形(适配商品图、卡片图)
square // 方形(适配普通图片、横幅)
}
class ImageLoaderWidget extends StatefulWidget {
// 必选参数:图片核心配置
final String imageUrl; // 网络图片地址
final double width; // 图片宽度(必填,避免适配混乱)
final double height; // 图片高度(必填,避免适配混乱)
// 可选参数:形态与样式配置
final ImageShape shape; // 裁剪形态(默认方形)
final double radius; // 圆角半径(仅rounded形态生效,默认8px)
final Color placeholderColor; // 占位背景色(默认浅灰)
final Widget? placeholder; // 自定义占位组件(优先级高于默认占位)
final Widget? errorWidget; // 自定义失败组件(优先级高于默认失败组件)
final BoxFit fit; // 图片适配模式(默认cover,避免拉伸变形)
final Duration fadeDuration; // 淡入动画时长(默认300ms,视觉更自然)
final bool enableRetry; // 是否支持失败重试(默认true)
const ImageLoaderWidget({
super.key,
required this.imageUrl,
required this.width,
required this.height,
this.shape = ImageShape.square,
this.radius = 8.0,
this.placeholderColor = const Color(0xFFF5F5F5),
this.placeholder,
this.errorWidget,
this.fit = BoxFit.cover,
this.fadeDuration = const Duration(milliseconds: 300),
this.enableRetry = true,
});
@override
State<ImageLoaderWidget> createState() => _ImageLoaderWidgetState();
}
class _ImageLoaderWidgetState extends State<ImageLoaderWidget> {
bool _isLoading = true; // 加载状态标识
bool _isError = false; // 加载错误标识
/// 重试加载图片(失败后点击触发)
void _retryLoad() {
setState(() {
_isLoading = true;
_isError = false;
});
}
/// 构建裁剪容器(根据形态适配裁剪方式)
Widget _buildClipContainer(Widget child) {
switch (widget.shape) {
case ImageShape.circle:
return ClipOval(child: child); // 圆形裁剪(头像专用)
case ImageShape.rounded:
return ClipRRect( // 圆角裁剪(商品图专用)
borderRadius: BorderRadius.circular(widget.radius),
child: child,
);
case ImageShape.square:
return child; // 无裁剪(普通图片)
}
}
/// 构建默认占位组件(加载中显示)
Widget _buildDefaultPlaceholder() {
return Container(
width: widget.width,
height: widget.height,
color: widget.placeholderColor,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2), // 加载指示器,体积更小
),
);
}
/// 构建默认失败组件(加载失败显示)
Widget _buildDefaultErrorWidget() {
return GestureDetector(
onTap: widget.enableRetry ? _retryLoad : null, // 支持点击重试
child: Container(
width: widget.width,
height: widget.height,
color: widget.placeholderColor,
child: const Center(
child: Icon(Icons.error_outline, color: Colors.grey, size: 32), // 友好错误图标
),
),
);
}
@override
Widget build(BuildContext context) {
// 根据状态切换核心内容:占位→图片→失败组件
Widget content;
if (_isLoading) {
// 加载中:优先使用自定义占位,无则用默认占位
content = widget.placeholder ?? _buildDefaultPlaceholder();
} else if (_isError) {
// 加载失败:优先使用自定义失败组件,无则用默认组件
content = widget.errorWidget ?? _buildDefaultErrorWidget();
} else {
// 加载成功:网络图片(绑定加载/错误回调)
content = Image.network(
widget.imageUrl,
width: widget.width,
height: widget.height,
fit: widget.fit,
// 加载中回调:保持占位显示
loadingBuilder: (context, child, progress) {
return progress == null ? child : _buildDefaultPlaceholder();
},
// 错误回调:切换错误状态
errorBuilder: (context, error, stackTrace) {
setState(() => _isError = true);
return _buildDefaultErrorWidget();
},
);
}
// 组合:裁剪容器 + 淡入动画(提升视觉体验)
return _buildClipContainer(
AnimatedOpacity(
opacity: _isLoading ? 0.0 : 1.0, // 加载中透明,加载完成淡入
duration: widget.fadeDuration,
curve: Curves.easeInOut, // 动画曲线更自然
child: content,
),
);
}
}
三、实战使用示例(覆盖 4 大高频场景)
场景 1:圆形头像(用户中心、评论区)
适配用户头像场景,圆形裁剪 + 简洁占位,符合主流设计风格:
dart
ImageLoaderWidget(
imageUrl: "https://example.com/avatar.jpg",
width: 60,
height: 60,
shape: ImageShape.circle, // 圆形裁剪
placeholderColor: Colors.grey[200],// 自定义占位色
fadeDuration: const Duration(milliseconds: 400), // 慢一点的动画
),
场景 2:圆角商品图(电商列表、商品详情)
商品图片常用圆角设计,避免尖锐感,适配卡片布局:
dart
ImageLoaderWidget(
imageUrl: "https://example.com/product.jpg",
width: 120,
height: 120,
shape: ImageShape.rounded, // 圆角裁剪
radius: 12, // 自定义圆角半径(更大更圆润)
fit: BoxFit.contain, // 商品图不拉伸,完整显示
placeholder: Container( // 自定义骨架屏占位
width: 120,
height: 120,
color: Colors.grey[100],
child: const Icon(Icons.shopping_bag_outlined, color: Colors.grey[300]),
),
),
场景 3:详情页横幅(带重试功能)
详情页大图加载失败影响体验,支持点击重试,降低用户流失:
dart
ImageLoaderWidget(
imageUrl: "https://example.com/detail-banner.jpg",
width: double.infinity, // 占满父容器宽度
height: 200,
shape: ImageShape.rounded,
radius: 8,
enableRetry: true, // 启用重试功能
errorWidget: Container( // 自定义失败提示
width: double.infinity,
height: 200,
color: Colors.grey[100],
child: const Center(
child: Text(
"图片加载失败,点击重试",
style: TextStyle(color: Colors.grey, fontSize: 16),
),
),
),
),
场景 4:普通方形图片(文章配图、消息图片)
无需裁剪的普通图片,保持原生形态,适配多样化场景:
dart
ImageLoaderWidget(
imageUrl: "https://example.com/article-img.jpg",
width: double.infinity,
height: 150,
shape: ImageShape.square, // 方形无裁剪
fit: BoxFit.cover, // 覆盖填充,保证图片饱满
placeholderColor: const Color(0xFFEEEEEE),
),
四、核心封装技巧(让组件更易用、更稳定)
- 状态管理清晰 :通过
_isLoading和_isError两个布尔值,精准控制 "占位→图片→失败" 三种状态切换,逻辑无冗余。 - 形态枚举化 :用
ImageShape枚举统一管理裁剪形态,避免硬编码,扩展新形态(如菱形)时只需新增枚举值,维护成本低。 - 优先级设计:自定义占位 / 失败组件优先级高于默认组件,既保证通用性,又支持个性化需求。
- 动画优化 :内置
AnimatedOpacity淡入动画,搭配Curves.easeInOut曲线,视觉过渡更自然,避免图片 "突然出现" 的突兀感。 - 重试机制:加载失败后支持点击重试,无需用户刷新页面,提升交互体验,尤其适合弱网场景。
五、避坑指南(实际开发必看)
- 尺寸必填原则 :宽度和高度必须明确设置(不建议用
double.infinity以外的动态值),避免图片适配混乱,尤其在列表中会导致布局抖动。 - 适配模式选择 :商品图、头像建议用
BoxFit.cover(填充不拉伸),文章配图、横幅建议用BoxFit.contain(完整显示)。 - 占位色适配:占位色建议与页面背景色协调,差异不宜过大,避免加载时出现 "闪屏" 视觉冲击。
- 长列表性能 :在
ListView.builder中使用时,建议设置cacheExtent优化滚动性能,同时避免图片尺寸过大(建议压缩至显示尺寸的 2 倍以内)。 - 重试功能慎用 :纯展示类图片(如历史消息图片)可禁用重试(
enableRetry: false),避免无效重试占用网络资源。
总结
ImageLoaderWidget通过 "通用化封装 + 个性化扩展" 的设计思路,彻底解决了原生Image.network的痛点,实现了 "一行代码加载图片,无需关心状态管理和样式适配"。无论是用户头像、商品图片,还是详情页横幅,都能通过简单配置快速实现,既保证了 UI 一致性,又大幅提升开发效率。