Skip to content

Fix null _nextFrame in MultiFrameImageStreamCompleter._handleAppFrame when a listener is re-added - #193575

Merged
auto-submit[bot] merged 12 commits into
flutter:masterfrom
Jstuff36:fix-image-stream-null-frame
Oct 7, 2026
Merged

auto-submit[bot] merged 12 commits into
flutter:masterfrom
Jstuff36:fix-image-stream-null-frame

Conversation

@Jstuff36

@Jstuff36 Jstuff36 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Title: Fix null _nextFrame in MultiFrameImageStreamCompleter._handleAppFrame when a listener is re-added


MultiFrameImageStreamCompleter._handleAppFrame can run after its decoded
frame has been thrown away. When that happens it dereferences a null
_nextFrame. In debug builds this fails the _nextFrame != null assertion; in
release builds it throws Null check operator used on a null value.

How it happens

  1. A frame is decoded and _handleAppFrame is scheduled as a frame callback.
  2. Before that frame runs, the stream drops to zero listeners and a listener is
    added back. _ImageState does this whenever TickerMode changes, for
    example during a route push or pop. Something must keep the completer alive
    while it has no listeners, as ImageCache does.
  3. removeListener cancels only _timer. It does not cancel the pending frame
    callback.
  4. addListener calls _decodeNextFrameAndSchedule(). That synchronously
    disposes _nextFrame and sets it to null before it awaits the next frame.
  5. The pending _handleAppFrame then runs with listeners present and
    _nextFrame == null.

#30346 fixed #30340 by stopping _handleAppFrame from being registered twice
(_frameCallbackScheduled). It did not handle the callback that is already
pending when decoding restarts; that PR's test for this scenario was merged
disabled. The same failure was reported again in #122576.

Fix

_handleAppFrame now returns early when _nextFrame is null. The restarted
decode sets _nextFrame and calls _scheduleAppFrame() when it completes.
_frameCallbackScheduled has already been reset at that point, so the animation
resumes on the next frame. The only visible effect is one skipped animation
frame instead of an exception. This follows the same pattern as the
_codec == null check added in #165009.

We see this crash in production in several apps that treat framework errors as
fatal. The trigger is animated images that are on screen during route
transitions.

Tests

Adds re-adding a listener while a frame callback is pending does not crash
to image_stream_test.dart. It reproduces the race without depending on
timing:

  • Without the fix it fails with
    Failed assertion: line 1064 pos 12: '_nextFrame != null'.
  • With the fix it passes, and the animation resumes once the restarted decode
    completes.
  • All other tests in image_stream_test.dart still pass.

Fixes #30340
Related: #122576, #30346

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Sep 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates MultiFrameImageStreamCompleter to handle cases where _nextFrame is null by returning early instead of asserting, which prevents crashes when a listener is re-added while a frame callback is pending. A regression test has been added to verify this behavior. The review feedback points out that calling addTearDown inside the asynchronous ImageStreamListener callback is an anti-pattern that can cause StateErrors, and suggests refactoring it to a single test-level teardown.

Comment on lines +593 to +597
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling addTearDown inside an asynchronous callback (like the ImageStreamListener) is an anti-pattern. If the callback is triggered after the test has started tearing down, it will throw a StateError. Instead, register a single addTearDown at the test level to dispose of all collected images.

Suggested change
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});
final emittedImages = <ImageInfo>[];
addTearDown(() {
for (final ImageInfo image in emittedImages) {
image.dispose();
}
});
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done: moved image disposal to a single test-level addTearDown. (The listener only fires during pump() within the test body, so this wasn't reachable in practice, but it's cleaner.)

@zanderso zanderso added the CICD Run CI/CD label Sep 30, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 30, 2026
@jmagman jmagman added the CICD Run CI/CD label Sep 30, 2026
justinmc
justinmc previously approved these changes Oct 1, 2026

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍

Comment on lines +617 to +618
imageStream.removeListener(listener);
imageStream.addListener(listener);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confirmed this will indeed trigger the relevant code 👍

Comment on lines +1064 to +1070
// A listener can be re-added after this callback was scheduled (for
// example when TickerMode toggles during a route transition). That
// restarts decoding in addListener, which discards _nextFrame. The
// in-flight decode schedules a new callback when it completes.
if (_nextFrame == null) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation, sounds right to me.

justinmc
justinmc previously approved these changes Oct 6, 2026

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re LGTM 👍

@justinmc justinmc added the CICD Run CI/CD label Oct 6, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 6, 2026
@justinmc justinmc added the CICD Run CI/CD label Oct 6, 2026
chunhtai
chunhtai previously approved these changes Oct 6, 2026

@chunhtai chunhtai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@chunhtai

chunhtai commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

looks like there is some foramt error @Jstuff36

@Jstuff36
Jstuff36 dismissed stale reviews from chunhtai and justinmc via 3ffbb68 October 6, 2026 21:24
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 6, 2026
@chunhtai chunhtai added the CICD Run CI/CD label Oct 6, 2026
@chunhtai
chunhtai requested a review from justinmc October 6, 2026 23:02

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 7, 2026
@chunhtai chunhtai added the CICD Run CI/CD label Oct 7, 2026
@justinmc justinmc added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 7, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into flutter:master with commit ea4ba77 Oct 7, 2026
29 of 30 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 7, 2026
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
… when a listener is re-added (flutter#193575)

**Title:** Fix null `_nextFrame` in
`MultiFrameImageStreamCompleter._handleAppFrame` when a listener is
re-added

---

`MultiFrameImageStreamCompleter._handleAppFrame` can run after its
decoded
frame has been thrown away. When that happens it dereferences a null
`_nextFrame`. In debug builds this fails the `_nextFrame != null`
assertion; in
release builds it throws `Null check operator used on a null value`.

### How it happens

1. A frame is decoded and `_handleAppFrame` is scheduled as a frame
callback.
2. Before that frame runs, the stream drops to zero listeners and a
listener is
added back. `_ImageState` does this whenever `TickerMode` changes, for
example during a route push or pop. Something must keep the completer
alive
   while it has no listeners, as `ImageCache` does.
3. `removeListener` cancels only `_timer`. It does not cancel the
pending frame
   callback.
4. `addListener` calls `_decodeNextFrameAndSchedule()`. That
synchronously
disposes `_nextFrame` and sets it to null before it awaits the next
frame.
5. The pending `_handleAppFrame` then runs with listeners present and
   `_nextFrame == null`.

flutter#30346 fixed flutter#30340 by stopping `_handleAppFrame` from being registered
twice
(`_frameCallbackScheduled`). It did not handle the callback that is
already
pending when decoding restarts; that PR's test for this scenario was
merged
disabled. The same failure was reported again in flutter#122576.

### Fix

`_handleAppFrame` now returns early when `_nextFrame` is null. The
restarted
decode sets `_nextFrame` and calls `_scheduleAppFrame()` when it
completes.
`_frameCallbackScheduled` has already been reset at that point, so the
animation
resumes on the next frame. The only visible effect is one skipped
animation
frame instead of an exception. This follows the same pattern as the
`_codec == null` check added in flutter#165009.

We see this crash in production in several apps that treat framework
errors as
fatal. The trigger is animated images that are on screen during route
transitions.

### Tests

Adds `re-adding a listener while a frame callback is pending does not
crash`
to `image_stream_test.dart`. It reproduces the race without depending on
timing:

- Without the fix it fails with
  `Failed assertion: line 1064 pos 12: '_nextFrame != null'`.
- With the fix it passes, and the animation resumes once the restarted
decode
  completes.
- All other tests in `image_stream_test.dart` still pass.

Fixes flutter#30340
Related: flutter#122576, flutter#30346

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

<!-- Keep the checklist link references from the upstream PR template.
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MultiFrameImageStreamCompleter null dereference

5 participants