Files
App.K12.Student.Miniapp_Flu…/docs/demos/image_demo.md
T
2026-09-04 15:22:20 +08:00

213 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】Image 图片组件(四种来源 + BoxFit + 缓存 + 错误兜底)
> 源文件:`lib/pages/demo/image_demo.dart` | 分类:组件封装
> 一句话:网络图(带缓存)/ 本地资源图 / 圆角图 / 圆形头像 -- 图片加载的四个必会姿势。
---
## 一、这个 Demo 解决什么问题
图片是 App 最高频的组件,但新手常踩:加载失败白屏、图片变形、圆角/圆形不会切、网络图每次重下载不缓存。
本页用四个区块讲清:
| 区块 | 用法 | 知识点 |
|---|---|---|
| ① 网络图片(带缓存) | `CachedNetworkImage` | `BoxFit.cover` + `progressIndicatorBuilder` 下载进度 + `errorWidget` 兜底 |
| ② 本地资源图 | `Image.asset` | 必须在 pubspec.yaml 注册 assets |
| ③ 圆角网络图 | `ClipRRect` 包 `CachedNetworkImage` | 图片本身没变,只是画出来带圆角 |
| ④ 圆形头像 | `CircleAvatar` + `CachedNetworkImageProvider` | `backgroundImage` 走缓存 + 失败时显示首字母 |
> 对照 Vue:`<img src>` 的 Flutter 版本,但多了"裁切模式"、"磁盘缓存"和"错误兜底"这三个必须处理的点。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: ImageDemoPage()));
```
- ① 首次加载显示下载进度条;再次进入本页(或同 URL 出现在列表页)秒出 -- 走本地缓存,不重新下载;
- ② 本地 logo 图等比完整显示;
- ③ 圆角大图(radius 60 接近胶囊),加载中转圈;
- ④ 圆形头像:加载失败时显示"头"字兜底。
- 断网/改错地址 -> 所有网络图都显示"图片加载失败"占位图(不白屏)。
---
## 三、核心知识点(新手向)
### 1. 图片来源的几种方式
```dart
CachedNetworkImage(imageUrl: url) // 网络图片 + 本地缓存(生产推荐)
CachedNetworkImageProvider(url) // 同上,但返回 ImageProvider(头像场景)
Image.network(url) // 网络图片(无缓存,每次重下载,仅作对比)
Image.asset('assets/...') // 本地资源(先注册 pubspec.yaml)
Image.file(File(...)) // 本地磁盘文件(需 path_provider,本页不演示)
Image.memory(bytes) // 内存字节流(base64 等,见 share_demo 截图预览)
```
### 2. 为什么用 CachedNetworkImage 而不是 Image.network
`Image.network` 每次都重新发 HTTP 请求下载图片,没缓存、列表页性能差、流量浪费。
`cached_network_image`(baseflow 维护,6.9k likes)底层用 `flutter_cache_manager` 把图片写入
本地缓存目录,二次加载直接读本地文件,对标 Web 端浏览器 HTTP 缓存。它还提供三段式回调,
比 `Image.network` 的 `loadingBuilder`(依赖 `ImageChunkEvent`,常常拿不到进度)更可靠:
```dart
CachedNetworkImage(
imageUrl: _netUrl,
fit: BoxFit.cover,
// 加载中占位 / 下载进度(两者二选一,progressIndicatorBuilder 更直观)
progressIndicatorBuilder: (context, url, downloadProgress) =>
CircularProgressIndicator(value: downloadProgress.progress),
// 失败兜底:断网/404/格式错 -> 占位图,不白屏
errorWidget: (context, url, error) => _buildErrorBox(),
)
```
> 注意:`placeholder` 和 `progressIndicatorBuilder` **二选一**,后者会覆盖前者。
> `downloadProgress.progress` 是 `double?`(总大小未知时为 null),需判空再赋给 `value`。
### 2.1. 新增依赖后必须 Hot Restart(原生端常见“假死 loading”根因)
现象:你改了 `pubspec.yaml`、新增插件后,只点 Hot Reload,Android/iOS 模拟器里可能出现
`CachedNetworkImage` 一直处于 loading(不进 errorWidget、不出图);但完整重启后恢复正常。
原因:原生插件注册发生在启动阶段(GeneratedPluginRegistrant),Hot Reload 不会重新执行注册流程。
解决:停止运行 → 重新 `flutter run` / Hot Restart 一次。
### 3. 头像场景用 CachedNetworkImageProvider
```dart
CircleAvatar(
backgroundImage: CachedNetworkImageProvider(_netUrl), // 同样走缓存
onBackgroundImageError: (error, stackTrace) {}, // 失败不崩
child: Text('头'), // 背景加载失败时显示首字母
)
```
`CachedNetworkImage` 是 Widget,适合直接渲染;`CachedNetworkImageProvider` 是 ImageProvider,
适合塞给 `CircleAvatar.backgroundImage`、`BoxDecoration.image` 等需要 ImageProvider 的场景。
### 4. BoxFit 裁切模式速记
| 值 | 行为 | 用途 |
|---|---|---|
| `cover` | 等比缩放填满并**裁掉多余** | 头像/封面(最常用) |
| `contain` | 等比缩放**完整显示**(有留白) | logo、二维码 |
| `fill` | **拉伸填满**(会变形) | 少用 |
| `fitWidth` / `fitHeight` | 按宽/高撑满 | 特殊场景 |
### 5. 错误兜底:不白屏
```dart
CachedNetworkImage(
imageUrl: _netUrl,
width: 300.w,
height: 180.h,
fit: BoxFit.cover,
errorWidget: (context, url, error) =>
_buildErrorBox(width: 300.w, height: 180.h), // 断网/404/格式错 -> 占位图
)
```
Flutter 3.x 网络图**务必给 `errorWidget`**,否则加载失败抛异常白屏。
占位图 `_buildErrorBox` 接收和原图一样的宽高 -> 布局不跳变(加载中->失败,UI 不抖动)。
### 6. 圆角 ≠ 图片属性,是"裁剪"组件
```dart
ClipRRect(
borderRadius: BorderRadius.circular(60.r), // 裁剪器
child: CachedNetworkImage(...), // 图片本身没变
)
```
---
## 四、关键代码剖析
```dart
class ImageDemoPage extends StatelessWidget {
const ImageDemoPage({super.key});
static const String _netUrl =
'https://hx.23544.com/StaticResource/2026-07-29/ranking3Position1785292048369.png';
/// 加载中占位:progress 为 null 转圈,非 null 显示百分比进度条
Widget _buildLoadingBox({
required double width,
required double height,
required double? progress,
}) {
return Container(
width: width,
height: height,
color: const Color(0xFFF0F2F5),
child: Center(
child: progress == null
? SizedBox(
width: 24.w,
height: 24.w,
child: const CircularProgressIndicator(strokeWidth: 2),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
value: progress, strokeWidth: 2),
),
SizedBox(height: 8.h),
Text('${(progress * 100).toInt()}%',
style: TextStyle(fontSize: 12.sp, color: Color(0xFF888888))),
],
),
),
);
}
/// 统一的加载失败占位图(接收和原图一样的宽高 -> 布局不跳变)
Widget _buildErrorBox({required double width, required double height}) {
return Container(
width: width,
height: height,
color: const Color(0xFFF0F2F5),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image_outlined, size: 40.w, color: Color(0xFFB0B4BC)),
SizedBox(height: 8.h),
Text('图片加载失败', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888))),
],
),
),
);
}
}
```
- `progressIndicatorBuilder` 用 `downloadProgress.progress` 驱动进度条,二次加载命中缓存时直接跳过该回调(秒出);
- 占位图接收和原图一样的宽高 -> 加载中 -> 成功/失败,UI 不抖动。
---
## 五、踩坑记录
1. **网络图必须 errorWidget/errorBuilder**:加载失败(断网/404/格式错)不给兜底就白屏;
`CachedNetworkImage` 用 `errorWidget`,`Image.network` 用 `errorBuilder`;
2. **新增插件后必须 Hot Restart**:只 Hot Reload 可能导致原生插件未重新注册,表现为一直 loading;
3. **Image.asset 必须先注册**:`pubspec.yaml` 的 `assets:` 列表里没有的路径直接运行期报错;
4. Android **release 包要 INTERNET 权限**(`AndroidManifest.xml` 已加)--debug 默认有,release 没有会黑图;
5. 图片"变形"先查 `fit`:忘了写默认是 `fill` 拉伸,封面图要用 `cover`;
6. 圆角用 `ClipRRect` 包,**不是**给图片加 borderRadius(Image 没有这个属性,Container 才有);
7. `placeholder` 与 `progressIndicatorBuilder` **二选一**:同时写后者生效,前者被忽略;
`downloadProgress.progress` 是 `double?`,总大小未知时为 null,直接塞给 `CircularProgressIndicator.value` 前要判空;
8. **缓存命中后不再回调进度**:二次加载直接读本地缓存,`progressIndicatorBuilder`/`placeholder` 都不会触发,
表现为"秒出" -- 这正是缓存生效的标志,不是没生效;
9. `Image.file` 在 Web 编译不过(`dart:io`),跨端项目用 `Image.memory` 替代(见 [note_picker_demo.md](note_picker_demo.md));
10. `cached_network_image` 在 **Web 端缓存能力有限**(仅内存缓存,刷新即失效),原生端(Android/iOS)走磁盘缓存。
---
## 六、扩展思考
- 多图选择页的缩略图渲染(原生/Web 双端)见 [note_picker_demo.md](note_picker_demo.md);
- 截图字节流用 `Image.memory` 预览见 [share_demo.md](share_demo.md);
- 富文本里的 `<img>` 渲染见 [rich_text_demo.md](rich_text_demo.md);
- 图片墙布局用 [grid_demo.md](grid_demo.md) 的 GridView;网络图片预览(双指缩放)用 `photo_view`。