通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97
图片是动漫App的核心内容,封面、角色头像、新闻配图到处都是图片。处理好图片加载,包括加载中状态、加载失败处理、尺寸控制,是提升用户体验的关键。
这篇文章会实现一个通用的网络图片组件,讲解 Image.network 的各种配置、加载状态处理,以及如何设计一个灵活可复用的图片组件。

为什么要封装图片组件
直接用 Image.network 有几个问题:
重复代码:每次都要写 loadingBuilder、errorBuilder。
不一致:不同地方的加载状态、错误处理可能不一样。
难维护:想统一修改加载样式,要改很多地方。
封装成组件后,这些问题都解决了。
组件基础结构
dart
import 'package:flutter/material.dart';
class AppNetworkImage extends StatelessWidget {
final String? imageUrl;
final double? width;
final double? height;
final BoxFit fit;
final Widget? placeholder;
final Widget? errorWidget;
const AppNetworkImage({
super.key,
required this.imageUrl,
this.width,
this.height,
this.fit = BoxFit.cover,
this.placeholder,
this.errorWidget,
});
imageUrl 是图片地址,可以为 null。
width 和 height 控制尺寸,可选。
fit 控制图片如何填充容器,默认 cover。
placeholder 和 errorWidget 是自定义的加载中和错误组件。
参数设计思路
必需 vs 可选:只有 imageUrl 是必需的,其他都有默认值。
默认值合理:fit 默认 cover 是最常用的,placeholder 和 errorWidget 默认为 null 使用内置样式。
可定制:需要自定义时可以传入 placeholder 和 errorWidget。
空 URL 处理
dart
@override
Widget build(BuildContext context) {
if (imageUrl == null || imageUrl!.isEmpty) {
return _buildError();
}
return Image.network(
imageUrl!,
// 其他配置
);
}
先检查 URL 是否为空,空的话直接显示错误状态,不发起网络请求。
这样可以避免不必要的网络请求和错误日志。
Image.network 配置
dart
return Image.network(
imageUrl!,
width: width,
height: height,
fit: fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return placeholder ?? Container(
width: width,
height: height,
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
},
errorBuilder: (context, error, stackTrace) {
return _buildError();
},
);
width 和 height 传给 Image.network,控制图片尺寸。
fit 控制图片如何填充容器。
loadingBuilder 处理加载中状态。
errorBuilder 处理加载失败。
loadingBuilder 详解
dart
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return placeholder ?? Container(
width: width,
height: height,
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
},
loadingProgress 为 null 表示加载完成,返回 child(图片本身)。
loadingProgress 不为 null 表示加载中,显示占位组件。
如果传入了自定义 placeholder 就用它,否则用默认的灰色背景 + 转圈圈。
加载进度显示
loadingProgress 包含加载进度信息:
dart
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
final progress = loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null;
return Container(
width: width,
height: height,
color: Colors.grey[300],
child: Center(
child: CircularProgressIndicator(
value: progress,
strokeWidth: 2,
),
),
);
},
cumulativeBytesLoaded 是已加载的字节数。
expectedTotalBytes 是总字节数,可能为 null(服务器没返回 Content-Length)。
有进度时显示确定的进度条,没有时显示不确定的转圈圈。
错误处理
dart
Widget _buildError() {
return errorWidget ?? Container(
width: width,
height: height,
color: Colors.grey[300],
child: const Icon(Icons.image, color: Colors.grey),
);
}
错误状态显示灰色背景 + 图片图标。
如果传入了自定义 errorWidget 就用它。
BoxFit 详解
BoxFit 控制图片如何填充容器:
dart
// 填满容器,可能裁剪
fit: BoxFit.cover
// 完整显示,可能留白
fit: BoxFit.contain
// 拉伸填满,可能变形
fit: BoxFit.fill
// 宽度填满
fit: BoxFit.fitWidth
// 高度填满
fit: BoxFit.fitHeight
// 不缩放
fit: BoxFit.none
cover 最常用,图片填满容器,超出部分裁剪,不会变形。
contain 完整显示图片,可能有留白。
fill 拉伸填满,会变形,一般不用。
组件的使用方式
dart
// 基本使用
AppNetworkImage(imageUrl: anime.imageUrl)
// 指定尺寸
AppNetworkImage(
imageUrl: anime.imageUrl,
width: 100,
height: 150,
)
// 自定义占位
AppNetworkImage(
imageUrl: anime.imageUrl,
placeholder: const ShimmerCard(),
)
// 自定义错误
AppNetworkImage(
imageUrl: anime.imageUrl,
errorWidget: Container(
color: Colors.red[100],
child: const Icon(Icons.error),
),
)
// 不同的填充方式
AppNetworkImage(
imageUrl: anime.imageUrl,
fit: BoxFit.contain,
)
一个组件,多种配置,适应各种场景。
圆角图片
配合 ClipRRect 实现圆角:
dart
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AppNetworkImage(
imageUrl: anime.imageUrl,
width: 100,
height: 150,
),
)
ClipRRect 裁剪子组件的圆角。
圆形图片
配合 ClipOval 实现圆形:
dart
ClipOval(
child: AppNetworkImage(
imageUrl: user.avatarUrl,
width: 60,
height: 60,
),
)
ClipOval 裁剪成椭圆,宽高相等时就是圆形。
深色模式适配
dart
Widget _buildError() {
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgColor = isDark ? Colors.grey[800] : Colors.grey[300];
final iconColor = isDark ? Colors.grey[600] : Colors.grey;
return errorWidget ?? Container(
width: width,
height: height,
color: bgColor,
child: Icon(Icons.image, color: iconColor),
);
}
深色模式下用深灰色背景,图标也用深灰色。
但这需要 context,StatelessWidget 里不能直接访问。可以改成 StatefulWidget 或者在 build 方法里处理。
添加淡入动画
图片加载完成后淡入显示:
dart
class AppNetworkImage extends StatefulWidget {
// 参数定义
}
class _AppNetworkImageState extends State<AppNetworkImage> {
bool _isLoaded = false;
@override
Widget build(BuildContext context) {
if (widget.imageUrl == null || widget.imageUrl!.isEmpty) {
return _buildError();
}
return AnimatedOpacity(
opacity: _isLoaded ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Image.network(
widget.imageUrl!,
width: widget.width,
height: widget.height,
fit: widget.fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_isLoaded) {
setState(() => _isLoaded = true);
}
});
return child;
}
return widget.placeholder ?? _buildPlaceholder();
},
errorBuilder: (context, error, stackTrace) {
return _buildError();
},
),
);
}
}
AnimatedOpacity 控制透明度动画。加载完成后设置 _isLoaded = true,触发淡入。
addPostFrameCallback 确保在帧结束后更新状态,避免在 build 过程中调用 setState。
图片缓存
Image.network 默认有内存缓存,但没有磁盘缓存。如果需要磁盘缓存,可以用 cached_network_image 包:
dart
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: anime.imageUrl ?? '',
width: width,
height: height,
fit: fit,
placeholder: (context, url) => _buildPlaceholder(),
errorWidget: (context, url, error) => _buildError(),
)
cached_network_image 会把图片缓存到磁盘,下次加载更快。
但在 HarmonyOS 上可能有兼容性问题,需要测试。
图片预加载
可以提前加载图片,用户看到时已经在缓存里:
dart
void _precacheImages(List<Anime> animeList) {
for (final anime in animeList) {
if (anime.imageUrl != null) {
precacheImage(NetworkImage(anime.imageUrl!), context);
}
}
}
precacheImage 预加载图片到内存缓存。
在列表数据加载完成后调用,用户滚动时图片已经准备好了。
图片尺寸优化
有些 API 支持请求不同尺寸的图片:
dart
String _getOptimizedUrl(String? url, {int? width}) {
if (url == null) return '';
// 假设 API 支持 ?w=200 参数
if (width != null) {
return '$url?w=$width';
}
return url;
}
请求小尺寸图片可以减少流量和加载时间。
图片加载失败重试
dart
class AppNetworkImage extends StatefulWidget {
// 参数
}
class _AppNetworkImageState extends State<AppNetworkImage> {
int _retryCount = 0;
static const int _maxRetry = 3;
void _retry() {
if (_retryCount < _maxRetry) {
setState(() => _retryCount++);
}
}
@override
Widget build(BuildContext context) {
return Image.network(
'${widget.imageUrl}?retry=$_retryCount', // 加参数强制重新请求
// 其他配置
errorBuilder: (context, error, stackTrace) {
return GestureDetector(
onTap: _retry,
child: Container(
width: widget.width,
height: widget.height,
color: Colors.grey[300],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.refresh, color: Colors.grey),
const SizedBox(height: 4),
Text(
'点击重试',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
);
},
);
}
}
加载失败时显示重试按钮,点击后重新加载。
URL 加上 retry 参数强制重新请求,绕过缓存。
小结
图片加载组件涉及的技术点:Image.network 、loadingBuilder 、errorBuilder 、BoxFit 、ClipRRect 圆角 、ClipOval 圆形 、AnimatedOpacity 淡入 、precacheImage 预加载。
组件设计要点:处理空 URL、加载中状态、加载失败状态,提供合理的默认值,支持自定义。
图片是 App 的重要组成部分,处理好图片加载能显著提升用户体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net