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

9.5 KiB
Raw Blame History

【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 版本,但多了"裁切模式"、"磁盘缓存"和"错误兜底"这三个必须处理的点。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: ImageDemoPage()));
  • ① 首次加载显示下载进度条;再次进入本页(或同 URL 出现在列表页)秒出 -- 走本地缓存,不重新下载;
  • ② 本地 logo 图等比完整显示;
  • ③ 圆角大图(radius 60 接近胶囊),加载中转圈;
  • ④ 圆形头像:加载失败时显示"头"字兜底。
  • 断网/改错地址 -> 所有网络图都显示"图片加载失败"占位图(不白屏)。

三、核心知识点(新手向)

1. 图片来源的几种方式

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,常常拿不到进度)更可靠:

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

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. 错误兜底:不白屏

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. 圆角 ≠ 图片属性,是"裁剪"组件

ClipRRect(
  borderRadius: BorderRadius.circular(60.r),  // 裁剪器
  child: CachedNetworkImage(...),              // 图片本身没变
)

四、关键代码剖析

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);
  10. cached_network_image 在 Web 端缓存能力有限(仅内存缓存,刷新即失效),原生端(Android/iOS)走磁盘缓存。

六、扩展思考