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.

    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.


    ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
      assert(key != null);
      assert(loader != null);
      TimelineTask timelineTask;
      TimelineTask listenerTask;
      if (!kReleaseMode) {
        timelineTask = TimelineTask()..start(
          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, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
        _cache[key] = image;
        return image.completer;
      final _CachedImage liveImage = _liveImages[key];
      if (liveImage != null) {
        _touch(key, liveImage, timelineTask);
        if (!kReleaseMode) {
          timelineTask.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
        return liveImage.completer;
      try {
        result = loader();
        _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
      } 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 {
      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) {
        // Images that fail to load don't contribute to cache size.
        final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
        final _CachedImage image = _CachedImage(result, imageSize);
            () => _liveImages.remove(key),
        final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
        if (pendingImage != null) {
        // Only touch if the cache was enabled when resolve was initially called.
        if (untrackedPendingImage == null) {
          _touch(key, image, listenerTask);
        if (!kReleaseMode && !listenedOnce) {
          listenerTask.finish(arguments: <String, dynamic>{
            'syncCall': syncCall,
            'sizeInBytes': imageSize,
          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].
      return result;