putIfAbsent method

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

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.

The arguments must not be null. The loader cannot return null.

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.

Implementation

ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
  assert(key != null);
  assert(loader != null);
  TimelineTask? timelineTask;
  TimelineTask? listenerTask;
  if (!kReleaseMode) {
    timelineTask = 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) {
      timelineTask!.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) {
      timelineTask!.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,
      ),
      timelineTask,
    );
    if (!kReleaseMode) {
      timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
    }
    return liveImage.completer;
  }

  try {
    result = loader();
    _trackLiveImage(key, result, null);
  } catch (error, stackTrace) {
    if (!kReleaseMode) {
      timelineTask!.finish(arguments: <String, dynamic>{
        'result': 'error',
        'error': error.toString(),
        'stackTrace': stackTrace.toString(),
      });
    }
    if (onError != null) {
      onError(error, stackTrace);
      return null;
    } else {
      rethrow;
    }
  }

  if (!kReleaseMode) {
    listenerTask = TimelineTask(parent: timelineTask)..start('listener');
  }
  // If we're doing tracing, we need to make sure that we don't try to finish
  // the trace entry multiple times if we get re-entrant calls from a multi-
  // frame provider here.
  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.
  // If the cache is disabled, this variable will be set.
  _PendingImage? untrackedPendingImage;
  void listener(ImageInfo? info, bool syncCall) {
    int? sizeBytes;
    if (info != null) {
      if (info is PowerImageInfo) {
        sizeBytes = info.width! * info.height! * 4;
      } else {
        sizeBytes = info.image.height * info.image.width * 4;
      }
      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 (untrackedPendingImage == null) {
      _touch(key, image, listenerTask);
    } else {
      image.dispose();
    }

    final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
    if (pendingImage != null) {
      pendingImage.removeListener();
    }
    if (!kReleaseMode && !listenedOnce) {
      listenerTask!.finish(arguments: <String, dynamic>{
        'syncCall': syncCall,
        'sizeInBytes': sizeBytes,
      });
      timelineTask!.finish(arguments: <String, dynamic>{
        'currentSizeBytes': currentSizeBytes,
        'currentSize': currentSize,
      });
    }
    listenedOnce = true;
  }

  final ImageStreamListener streamListener = ImageStreamListener(listener);
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    _pendingImages[key] = _PendingImage(result, streamListener);
  } else {
    untrackedPendingImage = _PendingImage(result, streamListener);
  }
  // Listener is removed in [_PendingImage.removeListener].
  result.addListener(streamListener);

  return result;
}