Remove assert from Image._handleImageFrame() (#33602)

Tickers being disabled and re-enabled can cause the
condition of a synchronous notification happening after
image frames have been delivered, which is valid in that
case. As such, this removes the assert.

https://github.com/flutter/flutter/issues/32374
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart
index e6ae97d..c77645f 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -936,7 +936,6 @@
   }
 
   void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
-    assert(_frameNumber == null || !synchronousCall);
     setState(() {
       _imageInfo = imageInfo;
       _loadingProgress = null;
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index 93fb6a5..290a959 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -844,12 +844,12 @@
     expect(lastFrame, isNull);
     expect(find.byType(Center), findsOneWidget);
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: await nextFrame()));
+    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
     await tester.pump();
     expect(lastFrame, 0);
     expect(find.byType(Center), findsOneWidget);
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: await nextFrame()));
+    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
     await tester.pump();
     expect(lastFrame, 1);
     expect(find.byType(Center), findsOneWidget);
@@ -877,7 +877,7 @@
     expect(lastFrame, isNull);
     expect(lastFrameWasSync, isFalse);
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: image));
+    streamCompleter.setData(imageInfo: ImageInfo(image: image));
     await tester.pump();
     expect(lastFrame, 0);
     expect(lastFrameWasSync, isFalse);
@@ -904,7 +904,7 @@
     expect(lastFrame, 0);
     expect(lastFrameWasSync, isTrue);
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: image));
+    streamCompleter.setData(imageInfo: ImageInfo(image: image));
     await tester.pump();
     expect(lastFrame, 1);
     expect(lastFrameWasSync, isTrue);
@@ -942,6 +942,79 @@
     expect(tester.state(find.byType(Image)), same(state));
   });
 
+  testWidgets('Image state handles enabling and disabling of tickers', (WidgetTester tester) async {
+    final ui.Codec codec = await tester.runAsync(() {
+      return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif));
+    });
+
+    Future<ui.Image> nextFrame() async {
+      final ui.FrameInfo frameInfo = await tester.runAsync(codec.getNextFrame);
+      return frameInfo.image;
+    }
+
+    final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
+    final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
+    int lastFrame;
+    int buildCount = 0;
+
+    Widget buildFrame(BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
+      lastFrame = frame;
+      buildCount++;
+      return child;
+    }
+
+    await tester.pumpWidget(
+      TickerMode(
+        enabled: true,
+        child: Image(
+          image: imageProvider,
+          frameBuilder: buildFrame,
+        ),
+      ),
+    );
+
+    final State<Image> state = tester.state(find.byType(Image));
+    expect(lastFrame, isNull);
+    expect(buildCount, 1);
+    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
+    await tester.pump();
+    expect(lastFrame, 0);
+    expect(buildCount, 2);
+
+    await tester.pumpWidget(
+      TickerMode(
+        enabled: false,
+        child: Image(
+          image: imageProvider,
+          frameBuilder: buildFrame,
+        ),
+      ),
+    );
+
+    expect(tester.state(find.byType(Image)), same(state));
+    expect(lastFrame, 0);
+    expect(buildCount, 3);
+    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
+    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
+    await tester.pump();
+    expect(lastFrame, 0);
+    expect(buildCount, 3);
+
+    await tester.pumpWidget(
+      TickerMode(
+        enabled: true,
+        child: Image(
+          image: imageProvider,
+          frameBuilder: buildFrame,
+        ),
+      ),
+    );
+
+    expect(tester.state(find.byType(Image)), same(state));
+    expect(lastFrame, 1); // missed a frame because we weren't animating at the time
+    expect(buildCount, 4);
+  });
+
   testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async {
     final ui.Image image = await tester.runAsync(createTestImage);
     final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
@@ -966,19 +1039,19 @@
     expect(chunkEvents.length, 1);
     expect(chunkEvents.first, isNull);
     expect(tester.binding.hasScheduledFrame, isFalse);
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isTrue);
     await tester.pump();
     expect(chunkEvents.length, 2);
     expect(find.text('loading 10 / 100'), findsOneWidget);
     expect(find.byType(RawImage), findsNothing);
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 30, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 30, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isTrue);
     await tester.pump();
     expect(chunkEvents.length, 3);
     expect(find.text('loading 30 / 100'), findsOneWidget);
     expect(find.byType(RawImage), findsNothing);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: image));
+    streamCompleter.setData(imageInfo: ImageInfo(image: image));
     await tester.pump();
     expect(chunkEvents.length, 4);
     expect(find.byType(Text), findsNothing);
@@ -998,12 +1071,12 @@
     );
 
     expect(tester.binding.hasScheduledFrame, isFalse);
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isFalse);
-    streamCompleter.notifyListeners(imageInfo: ImageInfo(image: image));
+    streamCompleter.setData(imageInfo: ImageInfo(image: image));
     expect(tester.binding.hasScheduledFrame, isTrue);
     await tester.pump();
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isFalse);
     expect(find.byType(RawImage), findsOneWidget);
   });
@@ -1029,7 +1102,7 @@
     expect(find.byType(Padding), findsOneWidget);
     expect(find.byType(RawImage), findsOneWidget);
     expect(tester.widget<Padding>(find.byType(Padding)).child, isInstanceOf<RawImage>());
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     await tester.pump();
     expect(find.byType(Center), findsOneWidget);
     expect(find.byType(Padding), findsOneWidget);
@@ -1047,7 +1120,7 @@
     );
 
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isFalse);
     final State<Image> state = tester.state(find.byType(Image));
 
@@ -1063,7 +1136,7 @@
     expect(find.byType(Center), findsOneWidget);
     expect(find.byType(RawImage), findsOneWidget);
     expect(tester.state(find.byType(Image)), same(state));
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isTrue);
     await tester.pump();
     expect(find.byType(Center), findsOneWidget);
@@ -1085,7 +1158,7 @@
 
     expect(find.byType(Center), findsOneWidget);
     expect(find.byType(RawImage), findsOneWidget);
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isTrue);
     await tester.pump();
     expect(find.byType(Center), findsOneWidget);
@@ -1099,7 +1172,7 @@
     expect(find.byType(Center), findsNothing);
     expect(find.byType(RawImage), findsOneWidget);
     expect(tester.state(find.byType(Image)), same(state));
-    streamCompleter.notifyListeners(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
+    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
     expect(tester.binding.hasScheduledFrame, isFalse);
   });
 }
@@ -1141,16 +1214,17 @@
 }
 
 class TestImageStreamCompleter extends ImageStreamCompleter {
-  TestImageStreamCompleter([this.synchronousImage]);
+  TestImageStreamCompleter([this._currentImage]);
 
-  final ImageInfo synchronousImage;
+  ImageInfo _currentImage;
   final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
 
   @override
   void addListener(ImageStreamListener listener) {
     listeners.add(listener);
-    if (synchronousImage != null)
-      listener.onImage(synchronousImage, true);
+    if (_currentImage != null) {
+      listener.onImage(_currentImage, true);
+    }
   }
 
   @override
@@ -1158,10 +1232,13 @@
     listeners.remove(listener);
   }
 
-  void notifyListeners({
+  void setData({
     ImageInfo imageInfo,
     ImageChunkEvent chunkEvent,
   }) {
+    if (imageInfo != null) {
+      _currentImage = imageInfo;
+    }
     final List<ImageStreamListener> localListeners = listeners.toList();
     for (ImageStreamListener listener in localListeners) {
       if (imageInfo != null) {