🚀 Flutter 图片预加载工具详解:支持并发加载、状态监听、SVG兼容
在构建 Flutter 应用时,提前加载图片资源是一种常见优化手段,尤其适用于展示型页面、动画首帧、轮播图等场景。本文将手把手讲解一个支持 批量并发预加载、状态追踪、SVG 加载支持 的工具类 ImagePreloader,帮助你打造更加丝滑的用户体验。
🎯 主要功能
| 功能 | 支持 |
|---|---|
| 图片预加载(本地/网络) | ✅ |
| SVG 图片预加载 | ✅ |
| 状态缓存与查询 | ✅ |
| 批量并发控制 | ✅ |
| 加载状态流用于 UI 监听 | ✅ |
| 手动清除与资源释放 | ✅ |
1️⃣ 状态枚举:让图片“有迹可循” 📍
// 图片预加载状态枚举
enum ImagePreloadStatus {
notStarted, // 未开始
loading, // 加载中
success, // 加载成功
failed, // 加载失败
}
- 设计动机:用 4 个离散值就能完整描述下载生命周期。
- 实战价值:UI 只要
switch(status)就能渲染骨架屏 / 进度条 / 错误页。
2️⃣ 结果模型:所有信息都装进一只“快递盒” 📦
class ImagePreloadResult {
final String imageUrl; // 图片 URL
final ImagePreloadStatus status; // 当前状态
final String? errorMessage; // 失败原因
final double progress; // 进度 0.0~1.0
ImagePreloadResult copyWith({ ... }) // 不可变 -> 复制更新
}
- 不可变 +
copyWith:每次状态更新都生成新实例,Stream才能检测到变化💡。 progress:SVG & 位图最终都会写 1.0,后续可扩展成真正的分帧进度。
3️⃣ 单例管理器:全局只开一张“管控面板” 🛠️
class ImagePreloader {
static final ImagePreloader _instance = ImagePreloader._internal();
factory ImagePreloader() => _instance; // 🔑 对外使用
ImagePreloader._internal();
final Map<String, ImagePreloadResult> _imageStatusMap = {};
final _imageStatusController = StreamController<ImagePreloadResult>.broadcast();
Stream<ImagePreloadResult> get imageStatusStream => _imageStatusController.stream;
}
-
为什么用单例?
- 同一张图 只下载一次,避免多页面重复 I/O。
broadcast()让多个StreamBuilder同时监听,而不用各自拉流。
4️⃣ 识别 & 缓存 SVG:走“专用绿色通道” 🛣️
bool _isSvgImage(String url) => url.toLowerCase().contains('.svg');
Future<void> _precacheSvg(String imageUrl) async {
final loader = imageUrl.startsWith('http')
? SvgNetworkLoader(imageUrl)
: SvgAssetLoader(imageUrl);
await svg.cache.putIfAbsent( // flutter_svg 的内部缓存表
loader.cacheKey(null),
() => loader.loadBytes(null),
);
}
- 区别:SVG 不走
precacheImage,直接把字节流塞进flutter_svg自带缓存⚡。 - 好处:跳过位图解码步骤,节省 CPU & 内存。
5️⃣ 核心流程 preloadImage(): 🏗️
Future<ImagePreloadResult> preloadImage(
String imageUrl,
BuildContext context, {
ImageErrorListener? onError,
}) async {
// 1. 命中缓存?
if (_imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success) {
return _imageStatusMap[imageUrl]!;
}
// 2. 先广播 loading
final result = ImagePreloadResult(imageUrl: imageUrl, status: ImagePreloadStatus.loading);
_imageStatusMap[imageUrl] = result;
_imageStatusController.add(result);
try {
if (_isSvgImage(imageUrl)) { // 3A. 走 SVG 流程
await _precacheSvg(imageUrl);
final ok = result.copyWith(status: ImagePreloadStatus.success, progress: 1.0);
_imageStatusMap[imageUrl] = ok;
_imageStatusController.add(ok);
return ok;
} else { // 3B. 走位图流程
final ImageProvider provider =
imageUrl.startsWith('http') ? NetworkImage(imageUrl) : AssetImage(imageUrl);
final completer = Completer<ImagePreloadResult>();
await precacheImage(provider, context, onError: (e, s) {
final fail = result.copyWith(status: ImagePreloadStatus.failed, errorMessage: e.toString());
_imageStatusMap[imageUrl] = fail;
_imageStatusController.add(fail);
onError?.call(e, s);
if (!completer.isCompleted) completer.complete(fail);
});
if (!completer.isCompleted) { // 成功分支
final ok = result.copyWith(status: ImagePreloadStatus.success, progress: 1.0);
_imageStatusMap[imageUrl] = ok;
_imageStatusController.add(ok);
completer.complete(ok);
}
return completer.future;
}
} catch (e) { // 4. 异常兜底
final fail = result.copyWith(status: ImagePreloadStatus.failed, errorMessage: e.toString());
_imageStatusMap[imageUrl] = fail;
_imageStatusController.add(fail);
return fail;
}
}
🔍 关注点
- 先查缓存 ➜ O(1) 返回,零耗时。
loading事件 出去后,UI 可立刻展示 Skeleton。- SVG / Bitmap 分流,二者公用同一套“成功 / 失败”广播逻辑。
Completer保证 同步代码 和 onError 回调 都能写入最终结果,不会出现“悬空 Future”。
6️⃣ 批量下载 preloadImages():并发池把洪水关进闸门 🌊🚪
Future<List<ImagePreloadResult>> preloadImages(
List<String> urls,
BuildContext ctx, {
int concurrency = 5, // 默认 5 条并发
}) async {
final pool = Pool(concurrency);
final results = <ImagePreloadResult>[];
await Future.wait(
urls.map((u) => pool.withResource(() async {
final r = await preloadImage(u, ctx);
results.add(r);
})),
);
return results;
}
pool.withResource():同时只放concurrency个任务在跑,其余排队。- 优势:避免一次下 50 张大图,导致带宽/内存峰值🚩。
7️⃣ 配套查询 & 清理:像缓存管理后台一样周全 🧹
bool isImagePreloaded(String url) => _imageStatusMap[url]?.status == ImagePreloadStatus.success;
void clearImage(String url) => _imageStatusMap.remove(url); // 删单张
void clearAllImages() => _imageStatusMap.clear(); // 全清
void dispose() => _imageStatusController.close(); // 组件卸载时调用
⚠️ 最佳实践:
- 若
ImagePreloader绑定到独立模块,可在模块dispose()时关闭 Stream。
🎯 一图胜千言:调用示例
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
flu
import 'image_preloader.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Preload Demo',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.teal),
home: const PreloadDemoPage(),
);
}
}
class PreloadDemoPage extends StatefulWidget {
const PreloadDemoPage({super.key});
@override
State<PreloadDemoPage> createState() => _PreloadDemoPageState();
}
class _PreloadDemoPageState extends State<PreloadDemoPage> {
bool _preloaded = false;
int _currentIndex = 0;
final _preloader = ImagePreloader();
List<String> noPreLoadImages = [
"http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg",
"http://c.hiphotos.baidu.com/image/pic/item/30adcbef76094b36de8a2fe5a1cc7cd98d109d99.jpg",
"http://h.hiphotos.baidu.com/image/pic/item/7c1ed21b0ef41bd5f2c2a9e953da81cb39db3d1d.jpg",
"http://g.hiphotos.baidu.com/image/pic/item/55e736d12f2eb938d5277fd5d0628535e5dd6f4a.jpg",
"http://e.hiphotos.baidu.com/image/pic/item/4e4a20a4462309f7e41f5cfe760e0cf3d6cad6ee.jpg",
"http://b.hiphotos.baidu.com/image/pic/item/9d82d158ccbf6c81b94575cfb93eb13533fa40a2.jpg",
"http://e.hiphotos.baidu.com/image/pic/item/4bed2e738bd4b31c1badd5a685d6277f9e2ff81e.jpg",
"http://g.hiphotos.baidu.com/image/pic/item/0d338744ebf81a4c87a3add4d52a6059252da61e.jpg",
"http://a.hiphotos.baidu.com/image/pic/item/f2deb48f8c5494ee5080c8142ff5e0fe99257e19.jpg",
];
List<String> preLoadImages = [
"http://f.hiphotos.baidu.com/image/pic/item/4034970a304e251f503521f5a586c9177e3e53f9.jpg",
"http://b.hiphotos.baidu.com/image/pic/item/279759ee3d6d55fbb3586c0168224f4a20a4dd7e.jpg",
"http://a.hiphotos.baidu.com/image/pic/item/e824b899a9014c087eb617650e7b02087af4f464.jpg",
"http://c.hiphotos.baidu.com/image/pic/item/9c16fdfaaf51f3de1e296fa390eef01f3b29795a.jpg",
"http://d.hiphotos.baidu.com/image/pic/item/b58f8c5494eef01f119945cbe2fe9925bc317d2a.jpg",
"http://h.hiphotos.baidu.com/image/pic/item/902397dda144ad340668b847d4a20cf430ad851e.jpg",
"http://b.hiphotos.baidu.com/image/pic/item/359b033b5bb5c9ea5c0e3c23d139b6003bf3b374.jpg",
"http://a.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a292d2472199d25bc315d607c7c.jpg",
"http://b.hiphotos.baidu.com/image/pic/item/e824b899a9014c08878b2c4c0e7b02087af4f4a3.jpg",
];
@override
void initState() {
super.initState();
_preloader.imageStatusStream.listen((result) {
debugPrint('[Stream] ${result.imageUrl} -> ${result.status}');
});
WidgetsBinding.instance.addPostFrameCallback((a){
_startPreload();
});
}
Future<void> _startPreload() async {
await _preloader.preloadImages(preLoadImages, context);
setState(() {
_preloaded = true;
});
}
Widget _buildImage(String url) {
if (url.endsWith('.svg')) {
return SvgPicture.network(
url,
placeholderBuilder: (_) => const CircularProgressIndicator(),
);
} else {
return Image.network(url, fit: BoxFit.cover);
}
}
Widget _buildPreloadedImage(String url) {
final status = _preloader.getImageStatus(url);
if (status?.status == ImagePreloadStatus.success) {
if (url.endsWith('.svg')) {
return SvgPicture.network(url);
} else {
return Image.network(url);
}
} else if (status?.status == ImagePreloadStatus.loading) {
return const Center(child: CircularProgressIndicator());
} else if (status?.status == ImagePreloadStatus.failed) {
return const Icon(Icons.error, color: Colors.red);
} else {
return const SizedBox.shrink();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("图片预加载对比 Demo")),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Text('❌ 未预加载'),
const SizedBox(height: 12),
Container(
width: double.infinity,
height: 300,
child: PageView.builder(
itemCount: noPreLoadImages.length,
itemBuilder: (context, index) {
return _buildImage(noPreLoadImages[index]);
}),
),
const SizedBox(width: 12),
const Text('✅ 预加载后显示'),
const SizedBox(height: 12),
Container(
width: double.infinity,
height: 300,
child: PageView.builder(
itemCount: preLoadImages.length,
itemBuilder: (context, index) {
return _buildPreloadedImage(preLoadImages[index]);
}),
),
],
),
),
);
}
}
- 一次调用 ➜ 全部图片进缓存,页面滑动时零闪烁。
- 只需一个
StreamBuilder,所有图片状态尽收眼底👀。
6️⃣ 完整代码
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pool/pool.dart';
// 图片预加载状态枚举
enum ImagePreloadStatus {
notStarted, // 未开始
loading, // 加载中
success, // 加载成功
failed, // 加载失败
}
// 图片预加载结果数据类
class ImagePreloadResult {
final String imageUrl; // 图片URL
final ImagePreloadStatus status; // 当前加载状态
final String? errorMessage; // 错误信息(失败时使用)
final double progress; // 加载进度(0.0-1.0)
ImagePreloadResult({
required this.imageUrl,
required this.status,
this.errorMessage,
this.progress = 0.0,
});
// 复制并更新属性的便捷方法
ImagePreloadResult copyWith({
String? imageUrl,
ImagePreloadStatus? status,
String? errorMessage,
double? progress,
}) {
return ImagePreloadResult(
imageUrl: imageUrl ?? this.imageUrl,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
progress: progress ?? this.progress,
);
}
}
// 图片预加载管理器(单例模式)
class ImagePreloader {
// 单例实例
static final ImagePreloader _instance = ImagePreloader._internal();
factory ImagePreloader() => _instance;
ImagePreloader._internal();
// 存储图片加载状态的映射表
final Map<String, ImagePreloadResult> _imageStatusMap = {};
// 用于广播加载状态变化的StreamController
final _imageStatusController =
StreamController<ImagePreloadResult>.broadcast();
// 暴露给外部的状态流
Stream<ImagePreloadResult> get imageStatusStream =>
_imageStatusController.stream;
// 获取不可修改的已加载图片状态映射
Map<String, ImagePreloadResult> get preloadedImages =>
Map.unmodifiable(_imageStatusMap);
// 判断是否为SVG图片
bool _isSvgImage(String imageUrl) {
return imageUrl.toLowerCase().contains('.svg');
}
// 预加载SVG图片
Future<void> _precacheSvg(String imageUrl) async {
final loader =
imageUrl.startsWith('http://') || imageUrl.startsWith('https://')
? SvgNetworkLoader(imageUrl)
: SvgAssetLoader(imageUrl);
await svg.cache.putIfAbsent(
loader.cacheKey(null),
() => loader.loadBytes(null),
);
}
// 核心方法:预加载单个图片
Future<ImagePreloadResult> preloadImage(
String imageUrl,
BuildContext context, {
ImageErrorListener? onError,
}) async {
// 如果图片已成功加载,直接返回缓存结果
if (_imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success) {
return _imageStatusMap[imageUrl]!;
}
// 初始化加载状态
final result = ImagePreloadResult(
imageUrl: imageUrl,
status: ImagePreloadStatus.loading,
);
_imageStatusMap[imageUrl] = result;
_imageStatusController.add(result);
try {
// 检查是否为SVG图片
if (_isSvgImage(imageUrl)) {
// 使用SVG专用方法预加载
await _precacheSvg(imageUrl);
// 成功处理
final successResult = result.copyWith(
status: ImagePreloadStatus.success,
progress: 1.0,
);
_imageStatusMap[imageUrl] = successResult;
_imageStatusController.add(successResult);
return successResult;
} else {
// 非SVG图片处理
// 根据URL类型创建对应的ImageProvider
ImageProvider imageProvider;
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
imageProvider = NetworkImage(imageUrl);
} else {
imageProvider = AssetImage(imageUrl);
}
final completer = Completer<ImagePreloadResult>();
// 使用Flutter原生方法预加载图片
await precacheImage(
imageProvider,
context,
onError: (exception, stackTrace) {
// 错误处理
final errorResult = result.copyWith(
status: ImagePreloadStatus.failed,
errorMessage: exception.toString(),
);
_imageStatusMap[imageUrl] = errorResult;
_imageStatusController.add(errorResult);
// 调用自定义错误回调
if (onError != null) {
onError(exception, stackTrace);
}
// 完成Completer
if (!completer.isCompleted) {
completer.complete(errorResult);
}
},
);
// 成功处理
if (!completer.isCompleted) {
final successResult = result.copyWith(
status: ImagePreloadStatus.success,
progress: 1.0,
);
_imageStatusMap[imageUrl] = successResult;
_imageStatusController.add(successResult);
completer.complete(successResult);
}
return await completer.future;
}
} catch (e) {
// 异常处理
final errorResult = result.copyWith(
status: ImagePreloadStatus.failed,
errorMessage: e.toString(),
);
_imageStatusMap[imageUrl] = errorResult;
_imageStatusController.add(errorResult);
return errorResult;
}
}
// 批量预加载方法
Future<List<ImagePreloadResult>> preloadImages(
List<String> imageUrls,
BuildContext context, {
ImageErrorListener? onError,
int concurrency = 5,
}) async {
final pool = Pool(concurrency);
final results = <ImagePreloadResult>[];
await Future.wait(
imageUrls.map(
(url) => pool.withResource(() async {
final result = await preloadImage(url, context, onError: onError);
results.add(result);
return result;
}),
),
);
return results;
}
// Future<List<ImagePreloadResult>> preloadImages(
// List<String> imageUrls,
// BuildContext context, {
// ImageErrorListener? onError,
// }) async {
// final futures = imageUrls.map(
// (url) => preloadImage(url, context, onError: onError),
// );
// return await Future.wait(futures);
// }
// Future<List<ImagePreloadResult>> preloadImages(
// List<String> imageUrls,
// BuildContext context, {
// ImageErrorListener? onError,
// int concurrency = 15,
// }) async {
// final results = <ImagePreloadResult>[];
// final queue = Queue.from(imageUrls);
// final activeTasks = <Future<void>>[];
// final completed = Completer<void>();
//
// void scheduleNext() {
// while (activeTasks.length < concurrency && queue.isNotEmpty) {
// final url = queue.removeFirst();
// late final Future<void> task;
// task = preloadImage(
// url,
// context,
// onError: onError,
// ).then((result) => results.add(result)).whenComplete(() {
// activeTasks.remove(task);
// scheduleNext();
// });
// activeTasks.add(task);
// }
//
// if (activeTasks.isEmpty && queue.isEmpty) {
// completed.complete();
// }
// }
//
// scheduleNext();
// await completed.future;
// return results;
// }
// 检查图片是否已预加载
bool isImagePreloaded(String imageUrl) {
return _imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success;
}
// 获取指定图片的状态
ImagePreloadResult? getImageStatus(String imageUrl) {
return _imageStatusMap[imageUrl];
}
// 清除单个图片缓存
void clearImage(String imageUrl) {
_imageStatusMap.remove(imageUrl);
}
// 清除所有图片缓存
void clearAllImages() {
_imageStatusMap.clear();
}
// 释放资源
void dispose() {
_imageStatusController.close();
}
}
🏁 收官小贴士
| 场景 | 建议 |
|---|---|
| 首屏 / Banner | 进入首页前 await preloadImages(),确保立刻可见。 |
| 无限列表 | 滑到第 N 页时把第 N+1 页的图片批量预热。 |
| 内存控制 | 大批量下载 ➜ 调低 concurrency;退出页面 ➜ clearAllImages()。 |