Skip to content

fix: Avoid decoding static images again when a listener is re-added - #1062

Merged
rickdijk merged 15 commits into
Baseflow:mainfrom
shihabkandil:fix/static-image-redecode-on-relisten
Oct 21, 2026
Merged

rickdijk merged 15 commits into
Baseflow:mainfrom
shihabkandil:fix/static-image-redecode-on-relisten

Conversation

@shihabkandil

@shihabkandil shihabkandil commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✨ What kind of change does this PR introduce? (Bug fix, feature, docs update...)

Bug fix

⤵️ What is the current behavior?

When an image stream loses all its listeners and gets one back, MultiImageStreamCompleter.addListener asks the codec for a new frame, even for a static image that was already decoded. This happens every time a route is covered and uncovered, because Image stops listening while TickerMode is off.

On native this is only wasted work. On web, in Firefox and Safari (where Flutter does not use the browser ImageDecoder), the second decode comes back blank, so images on a screen turn into empty boxes after navigating to another route and back.

🆕 What is the new behavior (if this is a feature change)?

  • A static image that already showed a frame is not decoded again when a listener is re-added, the listener just gets the current image. The check is the same one Flutter's MultiFrameImageStreamCompleter.addListener uses, but _currentImage is private in the framework so I used _framesEmitted instead, and it is now reset every time a new codec arrives.
  • A codec that comes directly from the stream now resets the frame count, so an animation after a placeholder shows all of its frames.
  • Frames from a codec that was replaced (still decoding or waiting to be shown) are thrown away, so an old cached image is not shown on top of the refreshed one anymore.
  • A failed decode doesn't block the next decodes, it's not retried until a listener is re-added.
  • A static frame that finishes decoding after the last listener left now goes to setImage instead of being dropped, so the next listener gets it without decoding again. The trade-off is that a completer kept alive only by the ImageCache keeps this decoded image even if it's never shown. It's the same memory an image that was already shown keeps.

💥 Does this PR introduce a breaking change?

No

🐛 Recommendations for testing

  • flutter test: the new test fails on main (numFramesAsked is 2) and passes with this change.
  • Manual: on web, open a page with a list of CachedNetworkImages in Firefox or Safari, push another route, pop back. Images stay visible with this change and go blank without it.

📝 Links to relevant issues/docs

Related: #949 replaces this completer with the framework one, which would also fix this. This PR is the minimal fix in the meantime.

🤔 Checklist before submitting

  • All projects build
  • Follows style guide lines (code style guide)
  • Relevant documentation was updated
  • Rebased onto current main

@shihabkandil

Copy link
Copy Markdown
Contributor Author

Can you please check this PR @rickdijk , i think it will solve the mentioned issue and this #1061 as well

@rickdijk rickdijk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more, outside the diff, at line 155 of multi_image_stream_completer.dart: if the last listener leaves while the first static decode is running, the decoded frame lands in _nextFrame and is dropped without being emitted. _framesEmitted stays 0, so the next addListener decodes again. On Firefox and Safari that is the blank-image path from the PR description, so navigating away mid-load and back can still blank the image. Calling setImage with the frame even when nobody is listening (it handles an empty listener list) would keep it for the next listener.

Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
Comment thread cached_network_image/test/image_stream_completer_test.dart
@rickdijk

Copy link
Copy Markdown
Collaborator

Once all comments are resolved, I'd like to ask you to bump the version of the package and update CHANGELOG.md too.

@shihabkandil

Copy link
Copy Markdown
Contributor Author

Thank you for your clarification, I will work on resolving it asap and dig deep in how it handles the multi frame images

@shihabkandil

Copy link
Copy Markdown
Contributor Author

@rickdijk I have resolved the different states of listeners adding / removing and re-addition in static, animated decoding states along with tests covering the 5 cases, if anything is still needed, let me know :))

@shihabkandil
shihabkandil requested a review from rickdijk October 19, 2026 20:15

@rickdijk rickdijk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up. The static-image fix looks right; the comments below are mostly about edge cases around it.

Two things on the description:

  • It says _framesEmitted is used, but the code adds _currentCodecHasImage.
  • The template checklist item reads "Rebased onto current develop". Please leave it as is rather than rewording it to main. Per AGENTS.md it is read as main, but the template text itself isn't edited in an unrelated PR.

One behaviour note, not introduced here: a finite animation that has finished restarts when a listener is re-added, because frameCount > 1 triggers a fresh decode. That matches the framework's condition, so I'm fine leaving it for #949.

Comment thread cached_network_image/pubspec.yaml Outdated
Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
Comment thread cached_network_image/CHANGELOG.md Outdated
Comment thread cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart Outdated
@shihabkandil

Copy link
Copy Markdown
Contributor Author

Thanks for picking this up. The static-image fix looks right; the comments below are mostly about edge cases around it.

Two things on the description:

* It says `_framesEmitted` is used, but the code adds `_currentCodecHasImage`.

* The template checklist item reads "Rebased onto current `develop`". Please leave it as is rather than rewording it to `main`. Per AGENTS.md it is read as `main`, but the template text itself isn't edited in an unrelated PR.

One behaviour note, not introduced here: a finite animation that has finished restarts when a listener is re-added, because frameCount > 1 triggers a fresh decode. That matches the framework's condition, so I'm fine leaving it for #949.

Thank you for your patience with my PR, I have applied the changes requested, let me know if anything still needs any work.

@shihabkandil
shihabkandil requested a review from rickdijk October 21, 2026 00:45
@rickdijk
rickdijk merged commit e0ca258 into Baseflow:main Oct 21, 2026
10 checks passed
@shihabkandil
shihabkandil deleted the fix/static-image-redecode-on-relisten branch October 21, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants