preface
One day the little sister took her mobile phone and said, “You have a problem downloading pictures here. Why don’t you have a network (open flight mode) and play Toast?”
Knee-jerk reaction, it must be Toast prompt play early, just click the button, Toast play before the start of download, hurry to take the mobile phone to operate and verify the wave. There is no network, hit the download complete prompt, go to the album to check, huh? Image download successful, and this operation? A quick look at the code shows images loaded by the cached_network_image tripartite library used in the project, which, as its name suggests, is a loading framework that caches network images. Therefore, the image should be cached locally after it is displayed, and the actual download process does not go through the network request. In order to verify the idea, I looked at the frame image loading process and summarized the following.
use
The CachedNetworkImage component can be used directly or through the ImageProvider.
Introduction of depend on
dependencies:
cached_network_image: ^ 3.1.0
Copy the code
Execute the flutter pub get, used in the project
Import it
import 'package:cached_network_image/cached_network_image.dart';
Copy the code
Add a placeholder map
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Copy the code
Progress bar display
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
progressIndicatorBuilder: (context, url, downloadProgress) =>
CircularProgressIndicator(value: downloadProgress.progress),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Copy the code
Native component Image coordination
Image(image: CachedNetworkImageProvider(url))
Copy the code
Use a placeholder map and provide providers for other components to use
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/200x150",
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
colorFilter:
ColorFilter.mode(Colors.red, BlendMode.colorBurn)),
),
),
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Copy the code
This will load the network image, and when the image is loaded, it will be cached locally. First look at the image loading process
The official website states that it does not currently include caching, which is actually implemented in another library, Flutter_cache_manager
The principle of
Load & Display
Here we only comb through the main process of image loading and caching, and do not make too much analysis of some other branch processes or irrelevant parameters
First, the constructor used on the page takes a mandatory parameter, imageUrl, which is used to generate the ImageProvider for image loading
class CachedNetworkImage extends StatelessWidget{
/// The image provided
final CachedNetworkImageProvider _image;
/// The constructor
CachedNetworkImage({
Key key,
@required this.imageUrl,
/// Omit the part
this.cacheManager,
/// .
}) : assert(imageUrl ! =null),
/// .
_image = CachedNetworkImageProvider(
imageUrl,
headers: httpHeaders,
cacheManager: cacheManager,
cacheKey: cacheKey,
imageRenderMethodForWeb: imageRenderMethodForWeb,
maxWidth: maxWidthDiskCache,
maxHeight: maxHeightDiskCache,
),
super(key: key);
@override
Widget build(BuildContext context) {
varoctoPlaceholderBuilder = placeholder ! =null ? _octoPlaceholderBuilder : null;
varoctoProgressIndicatorBuilder = progressIndicatorBuilder ! =null ? _octoProgressIndicatorBuilder : null;
/// .
return OctoImage(
image: _image,
/// .); }}Copy the code
Here you can see, the constructor initializes a local variable is CachedNetworkImageProvider _image types, it inherits ImageProvider provide loading images, see its constructor
/// Provide network picture loading Provider and cache
abstract class CachedNetworkImageProvider
extends ImageProvider<CachedNetworkImageProvider> {
/// Creates an object that fetches the image at the given URL.
const factory CachedNetworkImageProvider(
String url, {
int maxHeight,
int maxWidth,
String cacheKey,
double scale,
@Deprecated('ErrorListener is deprecated, use listeners on the imagestream')
ErrorListener errorListener,
Map<String.String> headers,
BaseCacheManager cacheManager,
ImageRenderMethodForWeb imageRenderMethodForWeb,
}) = image_provider.CachedNetworkImageProvider;
/// Optional cacheManager. Default value DefaultCacheManager()
/// CacheManager is not used when running on the Web.
BaseCacheManager get cacheManager;
/// The request url.
String get url;
/// The cache key
String get cacheKey;
/// .
@override
ImageStreamCompleter load(
CachedNetworkImageProvider key, DecoderCallback decode);
}
Copy the code
Its constructor calls image_provider. The example of the CachedNetworkImageProvider _image_provider_io. The dart is to load in the concrete implementation class
/// IO implementation of the CachedNetworkImageProvider; the ImageProvider to
/// load network images using a cache.
class CachedNetworkImageProvider
extends ImageProvider<image_provider.CachedNetworkImageProvider>
implements image_provider.CachedNetworkImageProvider {
/// Creates an ImageProvider which loads an image from the [url], using the [scale].
/// When the image fails to load [errorListener] is called.
const CachedNetworkImageProvider(
this.url, {
/// .
}) : assert(url ! =null),
assert(scale ! =null);
@override
final BaseCacheManager cacheManager;
/// .
@override
Future<CachedNetworkImageProvider> obtainKey(
ImageConfiguration configuration) {
return SynchronousFuture<CachedNetworkImageProvider>(this);
}
/// Core method load image entry
@override
ImageStreamCompleter load(
image_provider.CachedNetworkImageProvider key, DecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
/// Multiple load
return MultiImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProeperty<ImageProvider>(
'Image provider: $this \n Image key: $key'.this, style: DiagnosticsTreeStyle.errorProperty, ); }); }Copy the code
The load method here is the start point for loading images and is called when the page is visible
It returns a MultiImageStreamCompleter incoming _loadAsync, look at this method
/// Asynchronous loading
Stream<ui.Codec> _loadAsync(
CachedNetworkImageProvider key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async* {
assert(key == this);
try {
/// Default cache manager
var mngr = cacheManager ?? DefaultCacheManager();
assert(
mngr is ImageCacheManager || (maxWidth == null && maxHeight == null),
'To resize the image with a CacheManager the '
'CacheManager needs to be an ImageCacheManager. maxWidth and '
'maxHeight will be ignored when a normal CacheManager is used.');
/// The download logic is put in ImageCacheManager to get the download stream
var stream = mngr is ImageCacheManager
? mngr.getImageFile(key.url,
maxHeight: maxHeight,
maxWidth: maxWidth,
withProgress: true,
headers: headers,
key: key.cacheKey)
: mngr.getFileStream(key.url,
withProgress: true, headers: headers, key: key.cacheKey);
await for (var result in stream) {
if (result is FileInfo) {
var file = result.file;
var bytes = await file.readAsBytes();
var decoded = await decode(bytes);
/// The download result is returned
yielddecoded; }}}catch (e) {
/// .
} finally {
awaitchunkEvents.close(); }}}Copy the code
The DefaultCacheManager cacheManager is created using DefaultCacheManager.
The download logic is also placed under ImageCacheManager, and the result is a stream complete with support for multiple graph downloads, which are returned to the UI by yield to decode the final display.
MultiImageStreamCompleter support multiple load inherited from ImageStreamCompleter
/// An ImageStreamCompleter with support for loading multiple images.
class MultiImageStreamCompleter extends ImageStreamCompleter {
/// The constructor to create an MultiImageStreamCompleter. The [codec]
/// should be a stream with the images that should be shown. The
/// [chunkEvents] should indicate the [ImageChunkEvent]s of the first image
/// to show.
MultiImageStreamCompleter({
@required Stream<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : assert(codec ! =null),
_informationCollector = informationCollector,
_scale = scale {
/// According to the logical
codec.listen((event) {
if(_timer ! =null) {
_nextImageCodec = event;
} else {
_handleCodecReady(event);
}
}, onError: (dynamic error, StackTrace stack) {
reportError(
context: ErrorDescription('resolving an image codec'),
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,); });/// .}}/// Processing decoded complete
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec ! =null);
if(hasListeners) { _decodeNextFrameAndSchedule(); }}/// Decodes the next frame and draws
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(
context: ErrorDescription('resolving an image frame'),
exception: exception,
stack: stack,
informationCollector: _informationCollector,
silent: true,);return;
}
if (_codec.frameCount == 1) {
// ImageStreamCompleter listeners removed while waiting for next frame to
// be decoded.
// There's no reason to emit the frame without active listeners.
if(! hasListeners) {return;
}
// This is not an animated image, just return it and don't schedule more
// frames.
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return; } _scheduleAppFrame(); }}Copy the code
The display logic is done here, and the final frame is converted to the flutter. _scheduleAppFrame is used to send the frame
Download & Cache
The above MNGR calls the getImageFile method in ImageCacheManager and now it’s in the flutter_cache_manager three-party library, which is implicitly dependent on the file image_cache_Manager.dart
mixin ImageCacheManager on BaseCacheManager {
Stream<FileResponse> getImageFile(
String url, {
String key,
Map<String.String> headers,
bool withProgress,
int maxHeight,
int maxWidth,
}) async* {
if (maxHeight == null && maxWidth == null) {
yield* getFileStream(url,
key: key, headers: headers, withProgress: withProgress);
return;
}
/// .
}
/// .
}
Copy the code
The getFileStream method implements CacheManager in the subclass cache_Manager.dart file
class CacheManager implements BaseCacheManager {
/// Cache management
CacheStore _store;
/// Get the underlying store helper
CacheStore get store => _store;
/// Download manager
WebHelper _webHelper;
/// Get the underlying web helper
WebHelper get webHelper => _webHelper;
/// Reading a file from a download or cache returns a stream
@override
Stream<FileResponse> getFileStream(String url,
{String key, Map<String.String> headers, boolwithProgress}) { key ?? = url;final streamController = StreamController<FileResponse>();
_pushFileToStream(
streamController, url, key, headers, withProgress ?? false);
return streamController.stream;
}
Future<void> _pushFileToStream(StreamController streamController, String url,
String key, Map<String.String> headers, bool withProgress) async{ key ?? = url; FileInfo cacheFile;try {
/// Cache judgment
cacheFile = await getFileFromCache(key);
if(cacheFile ! =null) {
/// There is a cache return directly
streamController.add(cacheFile);
withProgress = false; }}catch (e) {
print(
'CacheManager: Failed to load cached file for $url with error:\n$e');
}
/// No caching or expired downloads
if (cacheFile == null || cacheFile.validTill.isBefore(DateTime.now())) {
try {
await for (var response
in _webHelper.downloadFile(url, key: key, authHeaders: headers)) {
if (response is DownloadProgress && withProgress) {
streamController.add(response);
}
if (response isFileInfo) { streamController.add(response); }}}catch (e) {
assert(() {
print(
'CacheManager: Failed to download file from $url with error:\n$e');
return true; } ());if (cacheFile == null&& streamController.hasListener) { streamController.addError(e); } } } unawaited(streamController.close()); }}Copy the code
Cache determination logic provides two levels of caching in the CacheStore
class CacheStore {
Duration cleanupRunMinInterval = const Duration(seconds: 10);
/// Cache completed without download
final _futureCache = <String, Future<CacheObject>>{};
/// The cache has been downloaded
final _memCache = <String, CacheObject>{};
/// .
Future<FileInfo> getFile(String key, {bool ignoreMemCache = false}) async {
final cacheObject =
await retrieveCacheData(key, ignoreMemCache: ignoreMemCache);
if (cacheObject == null || cacheObject.relativePath == null) {
return null;
}
final file = await fileSystem.createFile(cacheObject.relativePath);
return FileInfo(
file,
FileSource.Cache,
cacheObject.validTill,
cacheObject.url,
);
}
Future<void> putFile(CacheObject cacheObject) async {
_memCache[cacheObject.key] = cacheObject;
await _updateCacheDataInDatabase(cacheObject);
}
Future<CacheObject> retrieveCacheData(String key,
{bool ignoreMemCache = false}) async {
/// Check whether it has been cached
if(! ignoreMemCache && _memCache.containsKey(key)) {if (await _fileExists(_memCache[key])) {
return_memCache[key]; }}/// Uncached keys added to the futureCache are returned directly
if(! _futureCache.containsKey(key)) {final completer = Completer<CacheObject>();
/// Unadded to futureCache
unawaited(_getCacheDataFromDatabase(key).then((cacheObject) async {
if(cacheObject ! =null&&!await _fileExists(cacheObject)) {
final provider = await _cacheInfoRepository;
await provider.delete(cacheObject.id);
cacheObject = null;
}
_memCache[key] = cacheObject;
completer.complete(cacheObject);
unawaited(_futureCache.remove(key));
}));
_futureCache[key] = completer.future;
}
return _futureCache[key];
}
/// .
/// Update to database
Future<dynamic> _updateCacheDataInDatabase(CacheObject cacheObject) async {
final provider = await _cacheInfoRepository;
returnprovider.updateOrInsert(cacheObject); }}Copy the code
The _cacheInfoRepository cache repository is the database cache object used by the CacheObjectProvider
class CacheObjectProvider extends CacheInfoRepository
with CacheInfoRepositoryHelperMethods {
Database db;
String _path;
String databaseName;
CacheObjectProvider({String path, this.databaseName}) : _path = path;
/// Open the
@override
Future<bool> open() async {
if(! shouldOpenOnNewConnection()) {return openCompleter.future;
}
var path = await _getPath();
await File(path).parent.create(recursive: true);
db = await openDatabase(path, version: 3,
onCreate: (Database db, int version) async {
await db.execute('''
create table $_tableCacheObject (
${CacheObject.columnId} integer primary key,
${CacheObject.columnUrl} text,
${CacheObject.columnKey} text,
${CacheObject.columnPath} text,
${CacheObject.columnETag} text,
${CacheObject.columnValidTill} integer,
${CacheObject.columnTouched} integer,
${CacheObject.columnLength} integer
);
create unique index $_tableCacheObject${CacheObject.columnKey}
ON $_tableCacheObject (${CacheObject.columnKey}); ' ' ');
}, onUpgrade: (Database db, int oldVersion, int newVersion) async {
/// .
return opened();
}
@override
Future<dynamic> updateOrInsert(CacheObject cacheObject) {
if (cacheObject.id == null) {
return insert(cacheObject);
} else {
returnupdate(cacheObject); }}@override
Future<CacheObject> insert(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
var id = await db.insert(
_tableCacheObject,
cacheObject.toMap(setTouchedToNow: setTouchedToNow),
);
return cacheObject.copyWith(id: id);
}
@override
Future<CacheObject> get(String key) async {
List<Map> maps = await db.query(_tableCacheObject,
columns: null, where: '${CacheObject.columnKey} = ?', whereArgs: [key]);
if (maps.isNotEmpty) {
return CacheObject.fromMap(maps.first.cast<String.dynamic> ()); }return null;
}
@override
Future<int> delete(int id) {
return db.delete(_tableCacheObject,
where: '${CacheObject.columnId} = ?', whereArgs: [id]); }}Copy the code
The visible database cache is a CacheObject, which holds the URL, key, relativePath, and so on
class CacheObject {
static const columnId = '_id';
static const columnUrl = 'url';
static const columnKey = 'key';
static const columnPath = 'relativePath';
static const columnETag = 'eTag';
static const columnValidTill = 'validTill';
static const columnTouched = 'touched';
static const columnLength = 'length';
}
Copy the code
There is no cache call using the _webHelper.downloadFile method
class WebHelper {
WebHelper(this._store, FileService fileFetcher)
: _memCache = {},
fileFetcher = fileFetcher ?? HttpFileService();
final CacheStore _store;
@visibleForTesting
final FileService fileFetcher;
final Map<String, BehaviorSubject<FileResponse>> _memCache;
final Queue<QueueItem> _queue = Queue();
///Download the file from the url
Stream<FileResponse> downloadFile(String url,
{String key,
Map<String.String> authHeaders,
bool ignoreMemCache = false}) { key ?? = url;if(! _memCache.containsKey(key) || ignoreMemCache) {var subject = BehaviorSubject<FileResponse>();
_memCache[key] = subject;
/// Download or queue
unawaited(_downloadOrAddToQueue(url, key, authHeaders));
}
return _memCache[key].stream;
}
Future<void> _downloadOrAddToQueue(
String url,
String key,
Map<String.String> authHeaders,
) async {
// If too many requests are executed, queue and wait
if (concurrentCalls >= fileFetcher.concurrentFetches) {
_queue.add(QueueItem(url, key, authHeaders));
return;
}
concurrentCalls++;
var subject = _memCache[key];
try {
await for (var result
in_updateFile(url, key, authHeaders: authHeaders)) { subject.add(result); }}catch (e, stackTrace) {
subject.addError(e, stackTrace);
} finally {
concurrentCalls--;
awaitsubject.close(); _memCache.remove(key); _checkQueue(); }}///Download resources
Stream<FileResponse> _updateFile(String url, String key,
{Map<String.String> authHeaders}) async* {
var cacheObject = await _store.retrieveCacheData(key);
cacheObject = cacheObject == null
? CacheObject(url, key: key)
: cacheObject.copyWith(url: url);
/// Request a response
final response = await _download(cacheObject, authHeaders);
yield* _manageResponse(cacheObject, response);
}
Stream<FileResponse> _manageResponse(
CacheObject cacheObject, FileServiceResponse response) async* {
/// .
if (statusCodesNewFile.contains(response.statusCode)) {
int savedBytes;
await for (var progress in _saveFile(newCacheObject, response)) {
savedBytes = progress;
yield DownloadProgress(
cacheObject.url, response.contentLength, progress);
}
newCacheObject = newCacheObject.copyWith(length: savedBytes);
}
/// Join the cache
unawaited(_store.putFile(newCacheObject).then((_) {
if (newCacheObject.relativePath != oldCacheObject.relativePath) {
_removeOldFile(oldCacheObject.relativePath);
}
}));
final file = await _store.fileSystem.createFile(
newCacheObject.relativePath,
);
yield FileInfo(
file,
FileSource.Online,
newCacheObject.validTill,
newCacheObject.url,
);
}
Stream<int> _saveFile(CacheObject cacheObject, FileServiceResponse response) {
var receivedBytesResultController = StreamController<int> (); unawaited(_saveFileAndPostUpdates( receivedBytesResultController, cacheObject, response, ));return receivedBytesResultController.stream;
}
Future _saveFileAndPostUpdates(
StreamController<int> receivedBytesResultController,
CacheObject cacheObject,
FileServiceResponse response) async {
/// Create a file based on the path
final file = await _store.fileSystem.createFile(cacheObject.relativePath);
try {
var receivedBytes = 0;
/// Write files
final sink = file.openWrite();
await response.content.map((s) {
receivedBytes += s.length;
receivedBytesResultController.add(receivedBytes);
return s;
}).pipe(sink);
} catch (e, stacktrace) {
receivedBytesResultController.addError(e, stacktrace);
}
awaitreceivedBytesResultController.close(); }}Copy the code
conclusion
Cached_network_image Image-loading processes rely on ImageProvider. Caching and downloading logic is placed in a separate flutter_cache_manager download file that provides queue management in WebHelper. By default, HttpFileService is implemented. After downloading, the path is saved in CacheObject and stored in sqflite database