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