putIfAbsent method

ImageStreamCompleter? putIfAbsent(
  1. Object key,
  2. ImageStreamCompleter loader(
      ),
    1. {ImageErrorListener? onError}
    )

    Returns the previously cached ImageStream for the given key, if available; if not, calls the given callback to obtain it first. In either case, the key is moved to the 'most recently used' position.

    In the event that the loader throws an exception, it will be caught only if onError is also provided. When an exception is caught resolving an image, no completers are cached and null is returned instead of a new completer.

    Images that are larger than maximumSizeBytes are not cached, and do not cause other images in the cache to be evicted.

    Implementation

    ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
      TimelineTask? debugTimelineTask;
      if (!kReleaseMode) {
        debugTimelineTask = TimelineTask()..start(
          'ImageCache.putIfAbsent',
          arguments: <String, dynamic>{
            'key': key.toString(),
          },
        );
      }
      ImageStreamCompleter? result = _pendingImages[key]?.completer;
      // Nothing needs to be done because the image hasn't loaded yet.
      if (result != null) {
        if (!kReleaseMode) {
          debugTimelineTask!.finish(arguments: <String, dynamic>{'result': 'pending'});
        }
        return result;
      }
      // Remove the provider from the list so that we can move it to the
      // recently used position below.
      // Don't use _touch here, which would trigger a check on cache size that is
      // not needed since this is just moving an existing cache entry to the head.
      final _CachedImage? image = _cache.remove(key);
      if (image != null) {
        if (!kReleaseMode) {
          debugTimelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
        }
        // The image might have been keptAlive but had no listeners (so not live).
        // Make sure the cache starts tracking it as live again.
        _trackLiveImage(
          key,
          image.completer,
          image.sizeBytes,
        );
        _cache[key] = image;
        return image.completer;
      }
    
      final _LiveImage? liveImage = _liveImages[key];
      if (liveImage != null) {
        _touch(
          key,
          _CachedImage(
            liveImage.completer,
            sizeBytes: liveImage.sizeBytes,
          ),
          debugTimelineTask,
        );
        if (!kReleaseMode) {
          debugTimelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
        }
        return liveImage.completer;
      }
    
      try {
        result = loader();
        _trackLiveImage(key, result, null);
      } catch (error, stackTrace) {
        if (!kReleaseMode) {
          debugTimelineTask!.finish(arguments: <String, dynamic>{
            'result': 'error',
            'error': error.toString(),
            'stackTrace': stackTrace.toString(),
          });
        }
        if (onError != null) {
          onError(error, stackTrace);
          return null;
        } else {
          rethrow;
        }
      }
    
      if (!kReleaseMode) {
        debugTimelineTask!.start('listener');
      }
      // A multi-frame provider may call the listener more than once. We need do make
      // sure that some cleanup works won't run multiple times, such as finishing the
      // tracing task or removing the listeners
      bool listenedOnce = false;
    
      // We shouldn't use the _pendingImages map if the cache is disabled, but we
      // will have to listen to the image at least once so we don't leak it in
      // the live image tracking.
      final bool trackPendingImage = maximumSize > 0 && maximumSizeBytes > 0;
      late _PendingImage pendingImage;
      void listener(ImageInfo? info, bool syncCall) {
        int? sizeBytes;
        if (info != null) {
          sizeBytes = info.sizeBytes;
          info.dispose();
        }
        final _CachedImage image = _CachedImage(
          result!,
          sizeBytes: sizeBytes,
        );
    
        _trackLiveImage(key, result, sizeBytes);
    
        // Only touch if the cache was enabled when resolve was initially called.
        if (trackPendingImage) {
          _touch(key, image, debugTimelineTask);
        } else {
          image.dispose();
        }
    
        _pendingImages.remove(key);
        if (!listenedOnce) {
          pendingImage.removeListener();
        }
        if (!kReleaseMode && !listenedOnce) {
          debugTimelineTask!
            ..finish(arguments: <String, dynamic>{
              'syncCall': syncCall,
              'sizeInBytes': sizeBytes,
            })
            ..finish(arguments: <String, dynamic>{
              'currentSizeBytes': currentSizeBytes,
              'currentSize': currentSize,
            });
        }
        listenedOnce = true;
      }
    
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      pendingImage = _PendingImage(result, streamListener);
      if (trackPendingImage) {
        _pendingImages[key] = pendingImage;
      }
      // Listener is removed in [_PendingImage.removeListener].
      result.addListener(streamListener);
    
      return result;
    }