Flutter网络图片本地缓存如何实现

其他教程   发布日期:2025年03月24日   浏览次数:95

这篇文章主要讲解了“Flutter网络图片本地缓存如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Flutter网络图片本地缓存如何实现”吧!

一、问题

  1. Flutter
原有的图片缓存机制,是通过
  1. PaintingBinding.instance!.imageCache
来管理缓存的,这个缓存缓存到的是内存中,每次重新打开
  1. APP
或者缓存被清理都会再次进行网络请求,大图片加载慢不友好,且增加服务器负担。

二、思路

1、查看

  1. FadeInImage.assetNetwork
  1. Image.network
等几个网络请求的命名构造方法,初始化了
  1. ImageProvider
  1. FadeInImage.assetNetwork({
  2. Key key,
  3. @required String placeholder,
  4. this.placeholderErrorBuilder,
  5. @required String image,
  6. this.imageErrorBuilder,
  7. AssetBundle bundle,
  8. double placeholderScale,
  9. double imageScale = 1.0,
  10. this.excludeFromSemantics = false,
  11. this.imageSemanticLabel,
  12. this.fadeOutDuration = const Duration(milliseconds: 300),
  13. this.fadeOutCurve = Curves.easeOut,
  14. this.fadeInDuration = const Duration(milliseconds: 700),
  15. this.fadeInCurve = Curves.easeIn,
  16. this.width,
  17. this.height,
  18. this.fit,
  19. this.alignment = Alignment.center,
  20. this.repeat = ImageRepeat.noRepeat,
  21. this.matchTextDirection = false,
  22. int placeholderCacheWidth,
  23. int placeholderCacheHeight,
  24. int imageCacheWidth,
  25. int imageCacheHeight,
  26. }) : assert(placeholder != null),
  27. assert(image != null),
  28. placeholder = placeholderScale != null
  29. ? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
  30. : ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
  31. assert(imageScale != null),
  32. assert(fadeOutDuration != null),
  33. assert(fadeOutCurve != null),
  34. assert(fadeInDuration != null),
  35. assert(fadeInCurve != null),
  36. assert(alignment != null),
  37. assert(repeat != null),
  38. assert(matchTextDirection != null),
  39. image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
  40. super(key: key);
  1. Image.network(
  2. String src, {
  3. Key key,
  4. double scale = 1.0,
  5. this.frameBuilder,
  6. this.loadingBuilder,
  7. this.errorBuilder,
  8. this.semanticLabel,
  9. this.excludeFromSemantics = false,
  10. this.width,
  11. this.height,
  12. this.color,
  13. this.colorBlendMode,
  14. this.fit,
  15. this.alignment = Alignment.center,
  16. this.repeat = ImageRepeat.noRepeat,
  17. this.centerSlice,
  18. this.matchTextDirection = false,
  19. this.gaplessPlayback = false,
  20. this.filterQuality = FilterQuality.low,
  21. this.isAntiAlias = false,
  22. Map<String, String> headers,
  23. int cacheWidth,
  24. int cacheHeight,
  25. }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
  26. assert(alignment != null),
  27. assert(repeat != null),
  28. assert(matchTextDirection != null),
  29. assert(cacheWidth == null || cacheWidth > 0),
  30. assert(cacheHeight == null || cacheHeight > 0),
  31. assert(isAntiAlias != null),
  32. super(key: key);

其中:

  1. image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
,使用
  1. ImageProvider
类型的
  1. NetworkImage
创建了
  1. ImageProvider
类型的
  1. ResizeImage

  1. NetworkImage
是一个继承
  1. ImageProvider
的抽象类。
  1. abstract class NetworkImage extends ImageProvider<NetworkImage> {
  2. /// Creates an object that fetches the image at the given URL.
  3. ///
  4. /// The arguments [url] and [scale] must not be null.
  5. const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;
  6. /// The URL from which the image will be fetched.
  7. String get url;
  8. /// The scale to place in the [ImageInfo] object of the image.
  9. double get scale;
  10. /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
  11. ///
  12. /// When running flutter on the web, headers are not used.
  13. Map<String, String>? get headers;
  14. @override
  15. ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
  16. }

其中工厂方法给了一个值,

  1. const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;

进入

  1. network_image.NetworkImage
,到了
  1. _network_image_io.dart
文件。
  1. // Copyright 2014 The Flutter Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:async';
  5. import 'dart:io';
  6. import 'dart:typed_data';
  7. import 'dart:ui' as ui;
  8. import 'package:flutter/foundation.dart';
  9. import 'binding.dart';
  10. import 'debug.dart';
  11. import 'image_provider.dart' as image_provider;
  12. import 'image_stream.dart';
  13. /// The dart:io implementation of [image_provider.NetworkImage].
  14. @immutable
  15. class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
  16. /// Creates an object that fetches the image at the given URL.
  17. ///
  18. /// The arguments [url] and [scale] must not be null.
  19. const NetworkImage(this.url, { this.scale = 1.0, this.headers })
  20. : assert(url != null),
  21. assert(scale != null);
  22. @override
  23. final String url;
  24. @override
  25. final double scale;
  26. @override
  27. final Map<String, String>? headers;
  28. @override
  29. Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
  30. return SynchronousFuture<NetworkImage>(this);
  31. }
  32. @override
  33. ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
  34. // Ownership of this controller is handed off to [_loadAsync]; it is that
  35. // method's responsibility to close the controller's stream when the image
  36. // has been loaded or an error is thrown.
  37. final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
  38. return MultiFrameImageStreamCompleter(
  39. codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
  40. chunkEvents: chunkEvents.stream,
  41. scale: key.scale,
  42. debugLabel: key.url,
  43. informationCollector: () {
  44. return <DiagnosticsNode>[
  45. DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
  46. DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
  47. ];
  48. },
  49. );
  50. }
  51. // Do not access this field directly; use [_httpClient] instead.
  52. // We set `autoUncompress` to false to ensure that we can trust the value of
  53. // the `Content-Length` HTTP header. We automatically uncompress the content
  54. // in our call to [consolidateHttpClientResponseBytes].
  55. static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
  56. static HttpClient get _httpClient {
  57. HttpClient client = _sharedHttpClient;
  58. assert(() {
  59. if (debugNetworkImageHttpClientProvider != null)
  60. client = debugNetworkImageHttpClientProvider!();
  61. return true;
  62. }());
  63. return client;
  64. }
  65. Future<ui.Codec> _loadAsync(
  66. NetworkImage key,
  67. StreamController<ImageChunkEvent> chunkEvents,
  68. image_provider.DecoderCallback decode,
  69. ) async {
  70. try {
  71. assert(key == this);
  72. final Uri resolved = Uri.base.resolve(key.url);
  73. final HttpClientRequest request = await _httpClient.getUrl(resolved);
  74. headers?.forEach((String name, String value) {
  75. request.headers.add(name, value);
  76. });
  77. final HttpClientResponse response = await request.close();
  78. if (response.statusCode != HttpStatus.ok) {
  79. // The network may be only temporarily unavailable, or the file will be
  80. // added on the server later. Avoid having future calls to resolve
  81. // fail to check the network again.
  82. throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
  83. }
  84. final Uint8List bytes = await consolidateHttpClientResponseBytes(
  85. response,
  86. onBytesReceived: (int cumulative, int? total) {
  87. chunkEvents.add(ImageChunkEvent(
  88. cumulativeBytesLoaded: cumulative,
  89. expectedTotalBytes: total,
  90. ));
  91. },
  92. );
  93. if (bytes.lengthInBytes == 0)
  94. throw Exception('NetworkImage is an empty file: $resolved');
  95. return decode(bytes);
  96. } catch (e) {
  97. // Depending on where the exception was thrown, the image cache may not
  98. // have had a chance to track the key in the cache at all.
  99. // Schedule a microtask to give the cache a chance to add the key.
  100. scheduleMicrotask(() {
  101. PaintingBinding.instance!.imageCache!.evict(key);
  102. });
  103. rethrow;
  104. } finally {
  105. chunkEvents.close();
  106. }
  107. }
  108. @override
  109. bool operator ==(Object other) {
  110. if (other.runtimeType != runtimeType)
  111. return false;
  112. return other is NetworkImage
  113. && other.url == url
  114. && other.scale == scale;
  115. }
  116. @override
  117. int get hashCode => ui.hashValues(url, scale);
  118. @override
  119. String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
  120. }

对其中的

  1. _loadAsync
方法进行修改,实现图片的本地存储和获取,即可。

三、实现

1、新建一个文件

  1. my_local_cache_network_image.dart
,将
  1. _network_image_io.dart
内容复制过来,进行修改。 2、全部文件内容如下(非空安全版本):
  1. import 'dart:async';
  2. import 'dart:convert' as convert;
  3. import 'dart:io';
  4. import 'dart:typed_data';
  5. import 'dart:ui' as ui;
  6. import 'package:crypto/crypto.dart';
  7. import 'package:flutter/foundation.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:path_provider/path_provider.dart';
  10. /// The dart:io implementation of [image_provider.NetworkImage].
  11. @immutable
  12. class MyLocalCacheNetworkImage extends ImageProvider<NetworkImage> implements NetworkImage {
  13. /// Creates an object that fetches the image at the given URL.
  14. ///
  15. /// The arguments [url] and [scale] must not be null.
  16. const MyLocalCacheNetworkImage(
  17. this.url, {
  18. this.scale = 1.0,
  19. this.headers,
  20. this.isLocalCache = false,
  21. }) : assert(url != null),
  22. assert(scale != null);
  23. @override
  24. final String url;
  25. @override
  26. final double scale;
  27. @override
  28. final Map<String, String> headers;
  29. final bool isLocalCache;
  30. @override
  31. Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
  32. return SynchronousFuture<NetworkImage>(this);
  33. }
  34. @override
  35. ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
  36. // Ownership of this controller is handed off to [_loadAsync]; it is that
  37. // method's responsibility to close the controller's stream when the image
  38. // has been loaded or an error is thrown.
  39. final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
  40. return MultiFrameImageStreamCompleter(
  41. codec: _loadAsync(key, chunkEvents, decode),
  42. chunkEvents: chunkEvents.stream,
  43. scale: key.scale,
  44. debugLabel: key.url,
  45. informationCollector: () {
  46. return <DiagnosticsNode>[
  47. DiagnosticsProperty<ImageProvider>('Image provider', this),
  48. DiagnosticsProperty<NetworkImage>('Image key', key),
  49. ];
  50. },
  51. );
  52. }
  53. // Do not access this field directly; use [_httpClient] instead.
  54. // We set `autoUncompress` to false to ensure that we can trust the value of
  55. // the `Content-Length` HTTP header. We automatically uncompress the content
  56. // in our call to [consolidateHttpClientResponseBytes].
  57. static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
  58. static HttpClient get _httpClient {
  59. HttpClient client = _sharedHttpClient;
  60. assert(() {
  61. if (debugNetworkImageHttpClientProvider != null) client = debugNetworkImageHttpClientProvider();
  62. return true;
  63. }());
  64. return client;
  65. }
  66. Future<ui.Codec> _loadAsync(
  67. NetworkImage key,
  68. StreamController<ImageChunkEvent> chunkEvents,
  69. DecoderCallback decode,
  70. ) async {
  71. try {
  72. assert(key == this);
  73. /// 如果本地缓存过图片,直接返回图片
  74. if (isLocalCache != null && isLocalCache == true) {
  75. final Uint8List bytes = await _getImageFromLocal(key.url);
  76. if (bytes != null && bytes.lengthInBytes != null && bytes.lengthInBytes != 0) {
  77. return await PaintingBinding.instance.instantiateImageCodec(bytes);
  78. }
  79. }
  80. final Uri resolved = Uri.base.resolve(key.url);
  81. final HttpClientRequest request = await _httpClient.getUrl(resolved);
  82. headers?.forEach((String name, String value) {
  83. request.headers.add(name, value);
  84. });
  85. final HttpClientResponse response = await request.close();
  86. if (response.statusCode != HttpStatus.ok) {
  87. // The network may be only temporarily unavailable, or the file will be
  88. // added on the server later. Avoid having future calls to resolve
  89. // fail to check the network again.
  90. throw NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
  91. }
  92. final Uint8List bytes = await consolidateHttpClientResponseBytes(
  93. response,
  94. onBytesReceived: (int cumulative, int total) {
  95. chunkEvents.add(ImageChunkEvent(
  96. cumulativeBytesLoaded: cumulative,
  97. expectedTotalBytes: total,
  98. ));
  99. },
  100. );
  101. /// 网络请求结束后,将图片缓存到本地
  102. if (isLocalCache != null && isLocalCache == true && bytes.lengthInBytes != 0) {
  103. _saveImageToLocal(bytes, key.url);
  104. }
  105. if (bytes.lengthInBytes == 0) throw Exception('NetworkImage is an empty file: $resolved');
  106. return decode(bytes);
  107. } catch (e) {
  108. // Depending on where the exception was thrown, the image cache may not
  109. // have had a chance to track the key in the cache at all.
  110. // Schedule a microtask to give the cache a chance to add the key.
  111. scheduleMicrotask(() {
  112. PaintingBinding.instance.imageCache.evict(key);
  113. });
  114. rethrow;
  115. } finally {
  116. chunkEvents.close();
  117. }
  118. }
  119. /// 图片路径通过MD5处理,然后缓存到本地
  120. void _saveImageToLocal(Uint8List mUInt8List, String name) async {
  121. String path = await _getCachePathString(name);
  122. var file = File(path);
  123. bool exist = await file.exists();
  124. if (!exist) {
  125. File(path).writeAsBytesSync(mUInt8List);
  126. }
  127. }
  128. /// 从本地拿图片
  129. Future<Uint8List> _getImageFromLocal(String name) async {
  130. String path = await _getCachePathString(name);
  131. var file = File(path);
  132. bool exist = await file.exists();
  133. if (exist) {
  134. final Uint8List bytes = await file.readAsBytes();
  135. return bytes;
  136. }
  137. return null;
  138. }
  139. /// 获取图片的缓存路径并创建
  140. Future<String> _getCachePathString(String name) async {
  141. // 获取图片的名称
  142. String filePathFileName = md5.convert(convert.utf8.encode(name)).toString();
  143. String extensionName = name.split('/').last.split('.').last;
  144. // print('图片url:$name');
  145. // print('filePathFileName:$filePathFileName');
  146. // print('extensionName:$extensionName');
  147. // 生成、获取结果存储路径
  148. final tempDic = await getTemporaryDirectory();
  149. Directory directory = Directory(tempDic.path + '/CacheImage/');
  150. bool isFoldExist = await directory.exists();
  151. if (!isFoldExist) {
  152. await directory.create();
  153. }
  154. return directory.path + filePathFileName + '.$extensionName';
  155. }
  156. @override
  157. bool operator ==(Object other) {
  158. if (other.runtimeType != runtimeType) return false;
  159. return other is NetworkImage && other.url == url && other.scale == scale;
  160. }
  161. @override
  162. int get hashCode => ui.hashValues(url, scale);
  163. @override
  164. String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
  165. }

主要修改有: 1、从本地获取缓存并返回

  1. /// 如果本地缓存过图片,直接返回图片
  2. if (isLocalCache != null && isLocalCache == true) {
  3. final Uint8List bytes = await _getImageFromLocal(key.url);
  4. if (bytes != null && bytes.lengthInBytes != null && bytes.lengthInBytes != 0) {
  5. return await PaintingBinding.instance.instantiateImageCodec(bytes);
  6. }
  7. }

2、图片网络情请求完之后,存储到本地

  1. /// 网络请求结束后,将图片缓存到本地
  2. if (isLocalCache != null && isLocalCache == true && bytes.lengthInBytes != 0) {
  3. _saveImageToLocal(bytes, key.url);
  4. }

3、保存到本地、从本地获取图片、获取并创建本地缓存路径的具体实现,主要是最其中图片网络请求获取到的

  1. bytes
和图片的
  1. url
进行存储等操作。
  1. /// 图片路径通过MD5处理,然后缓存到本地
  2. void _saveImageToLocal(Uint8List mUInt8List, String name) async {
  3. String path = await _getCachePathString(name);
  4. var file = File(path);
  5. bool exist = await file.exists();
  6. if (!exist) {
  7. File(path).writeAsBytesSync(mUInt8List);
  8. }
  9. }
  10. /// 从本地拿图片
  11. Future<Uint8List> _getImageFromLocal(String name) async {
  12. String path = await _getCachePathString(name);
  13. var file = File(path);
  14. bool exist = await file.exists();
  15. if (exist) {
  16. final Uint8List bytes = await file.readAsBytes();
  17. return bytes;
  18. }
  19. return null;
  20. }
  21. /// 获取图片的缓存路径并创建
  22. Future<String> _getCachePathString(String name) async {
  23. // 获取图片的名称
  24. String filePathFileName = md5.convert(convert.utf8.encode(name)).toString();
  25. String extensionName = name.split('/').last.split('.').last;
  26. // print('图片url:$name');
  27. // print('filePathFileName:$filePathFileName');
  28. // print('extensionName:$extensionName');
  29. // 生成、获取结果存储路径
  30. final tempDic = await getTemporaryDirectory();
  31. Directory directory = Directory(tempDic.path + '/CacheImage/');
  32. bool isFoldExist = await directory.exists();
  33. if (!isFoldExist) {
  34. await directory.create();
  35. }
  36. return directory.path + filePathFileName + '.$extensionName';
  37. }

四、使用

将上面的命名构造方法复制出来,创建一个自己的命名构造方法,比如(部分代码):

  1. class CustomFadeInImage extends StatelessWidget {
  2. CustomFadeInImage.assetNetwork({
  3. @required this.image,
  4. this.placeholder,
  5. this.width,
  6. this.height,
  7. this.fit,
  8. this.alignment = Alignment.center,
  9. this.imageScale = 1.0,
  10. this.imageCacheWidth,
  11. this.imageCacheHeight,
  12. }) : imageProvider = ResizeImage.resizeIfNeeded(
  13. imageCacheWidth, imageCacheHeight, MyLocalCacheNetworkImage(image, scale: imageScale, isLocalCache: true));

  1. ResizeImage.resizeIfNeeded
中的
  1. NetworkImage
替换为
  1. MyLocalCacheNetworkImage
即可。

以上就是Flutter网络图片本地缓存如何实现的详细内容,更多关于Flutter网络图片本地缓存如何实现的资料请关注九品源码其它相关文章!