1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6///
7/// @docImport 'package:flutter/widgets.dart';
8///
9/// @docImport 'image_cache.dart';
10/// @docImport 'image_provider.dart';
11library;
12
13import 'dart:async';
14import 'dart:ui' as ui show Codec, FrameInfo, Image;
15
16import 'package:flutter/foundation.dart';
17import 'package:flutter/scheduler.dart';
18
19/// A [dart:ui.Image] object with its corresponding scale.
20///
21/// ImageInfo objects are used by [ImageStream] objects to represent the
22/// actual data of the image once it has been obtained.
23///
24/// The disposing contract for [ImageInfo] (as well as for [ui.Image])
25/// is different from traditional one, where
26/// an object should dispose a member if the object created the member.
27/// Instead, the disposal contract is as follows:
28///
29/// * [ImageInfo] disposes [image], even if it is received as a constructor argument.
30/// * [ImageInfo] is expected to be disposed not by the object, that created it,
31/// but by the object that owns reference to it.
32/// * It is expected that only one object owns reference to [ImageInfo] object.
33///
34/// Safety tips:
35///
36/// * To share the [ImageInfo] or [ui.Image] between objects, use the [clone] method,
37/// which will not clone the entire underlying image, but only reference to it and information about it.
38/// * After passing a [ui.Image] or [ImageInfo] reference to another object,
39/// release the reference.
40@immutable
41class ImageInfo {
42 /// Creates an [ImageInfo] object for the given [image] and [scale].
43 ///
44 /// The [debugLabel] may be used to identify the source of this image.
45 ///
46 /// See details for disposing contract in the class description.
47 ImageInfo({required this.image, this.scale = 1.0, this.debugLabel}) {
48 assert(debugMaybeDispatchCreated('painting', 'ImageInfo', this));
49 }
50
51 /// Creates an [ImageInfo] with a cloned [image].
52 ///
53 /// This method must be used in cases where a client holding an [ImageInfo]
54 /// needs to share the image info object with another client and will still
55 /// need to access the underlying image data at some later point, e.g. to
56 /// share it again with another client.
57 ///
58 /// See details for disposing contract in the class description.
59 ///
60 /// See also:
61 ///
62 /// * [ui.Image.clone], which describes how and why to clone images.
63 ImageInfo clone() {
64 return ImageInfo(image: image.clone(), scale: scale, debugLabel: debugLabel);
65 }
66
67 /// Whether this [ImageInfo] is a [clone] of the `other`.
68 ///
69 /// This method is a convenience wrapper for [ui.Image.isCloneOf], and is
70 /// useful for clients that are trying to determine whether new layout or
71 /// painting logic is required when receiving a new image reference.
72 ///
73 /// {@tool snippet}
74 ///
75 /// The following sample shows how to appropriately check whether the
76 /// [ImageInfo] reference refers to new image data or not (in this case in a
77 /// setter).
78 ///
79 /// ```dart
80 /// ImageInfo? get imageInfo => _imageInfo;
81 /// ImageInfo? _imageInfo;
82 /// set imageInfo (ImageInfo? value) {
83 /// // If the image reference is exactly the same, do nothing.
84 /// if (value == _imageInfo) {
85 /// return;
86 /// }
87 /// // If it is a clone of the current reference, we must dispose of it and
88 /// // can do so immediately. Since the underlying image has not changed,
89 /// // We don't have any additional work to do here.
90 /// if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
91 /// value.dispose();
92 /// return;
93 /// }
94 /// // It is a new image. Dispose of the old one and take a reference
95 /// // to the new one.
96 /// _imageInfo?.dispose();
97 /// _imageInfo = value;
98 /// // Perform work to determine size, paint the image, etc.
99 /// // ...
100 /// }
101 /// ```
102 /// {@end-tool}
103 bool isCloneOf(ImageInfo other) {
104 return other.image.isCloneOf(image) && scale == scale && other.debugLabel == debugLabel;
105 }
106
107 /// The raw image pixels.
108 ///
109 /// This is the object to pass to the [Canvas.drawImage],
110 /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
111 /// the image.
112 final ui.Image image;
113
114 /// The size of raw image pixels in bytes.
115 int get sizeBytes => image.height * image.width * 4;
116
117 /// The linear scale factor for drawing this image at its intended size.
118 ///
119 /// The scale factor applies to the width and the height.
120 ///
121 /// {@template flutter.painting.imageInfo.scale}
122 /// For example, if this is 2.0, it means that there are four image pixels for
123 /// every one logical pixel, and the image's actual width and height (as given
124 /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are
125 /// double the height and width that should be used when painting the image
126 /// (e.g. in the arguments given to [Canvas.drawImage]).
127 /// {@endtemplate}
128 final double scale;
129
130 /// A string used for debugging purposes to identify the source of this image.
131 final String? debugLabel;
132
133 /// Disposes of this object.
134 ///
135 /// Once this method has been called, the object should not be used anymore,
136 /// and no clones of it or the image it contains can be made.
137 void dispose() {
138 assert((image.debugGetOpenHandleStackTraces()?.length ?? 1) > 0);
139 assert(debugMaybeDispatchDisposed(this));
140 image.dispose();
141 }
142
143 @override
144 String toString() =>
145 '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
146
147 @override
148 int get hashCode => Object.hash(image, scale, debugLabel);
149
150 @override
151 bool operator ==(Object other) {
152 if (other.runtimeType != runtimeType) {
153 return false;
154 }
155 return other is ImageInfo &&
156 other.image == image &&
157 other.scale == scale &&
158 other.debugLabel == debugLabel;
159 }
160}
161
162/// Interface for receiving notifications about the loading of an image.
163///
164/// This class overrides [operator ==] and [hashCode] to compare the individual
165/// callbacks in the listener, meaning that if you add an instance of this class
166/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
167/// _different_ instance of this class when you remove the listener, and the
168/// listener will be properly removed as long as all associated callbacks are
169/// equal.
170///
171/// Used by [ImageStream] and [ImageStreamCompleter].
172@immutable
173class ImageStreamListener {
174 /// Creates a new [ImageStreamListener].
175 const ImageStreamListener(this.onImage, {this.onChunk, this.onError});
176
177 /// Callback for getting notified that an image is available.
178 ///
179 /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
180 /// that drives the notifications fires multiple times). An example of such a
181 /// case would be an image with multiple frames within it (such as an animated
182 /// GIF).
183 ///
184 /// For more information on how to interpret the parameters to the callback,
185 /// see the documentation on [ImageListener].
186 ///
187 /// See also:
188 ///
189 /// * [onError], which will be called instead of [onImage] if an error occurs
190 /// during loading.
191 final ImageListener onImage;
192
193 /// Callback for getting notified when a chunk of bytes has been received
194 /// during the loading of the image.
195 ///
196 /// This callback may fire many times (e.g. when used with a [NetworkImage],
197 /// where the image bytes are loaded incrementally over the wire) or not at
198 /// all (e.g. when used with a [MemoryImage], where the image bytes are
199 /// already available in memory).
200 ///
201 /// This callback may also continue to fire after the [onImage] callback has
202 /// fired (e.g. for multi-frame images that continue to load after the first
203 /// frame is available).
204 final ImageChunkListener? onChunk;
205
206 /// Callback for getting notified when an error occurs while loading an image.
207 ///
208 /// If an error occurs during loading, [onError] will be called instead of
209 /// [onImage].
210 ///
211 /// If [onError] is called and does not throw, then the error is considered to
212 /// be handled. An error handler can explicitly rethrow the exception reported
213 /// to it to safely indicate that it did not handle the exception.
214 ///
215 /// If an image stream has no listeners that handled the error when the error
216 /// was first encountered, then the error is reported using
217 /// [FlutterError.reportError], with the [FlutterErrorDetails.silent] flag set
218 /// to true.
219 final ImageErrorListener? onError;
220
221 @override
222 int get hashCode => Object.hash(onImage, onChunk, onError);
223
224 @override
225 bool operator ==(Object other) {
226 if (other.runtimeType != runtimeType) {
227 return false;
228 }
229 return other is ImageStreamListener &&
230 other.onImage == onImage &&
231 other.onChunk == onChunk &&
232 other.onError == onError;
233 }
234}
235
236/// Signature for callbacks reporting that an image is available.
237///
238/// Used in [ImageStreamListener].
239///
240/// The `image` argument contains information about the image to be rendered.
241/// The implementer of [ImageStreamListener.onImage] is expected to call dispose
242/// on the [ui.Image] it receives.
243///
244/// The `synchronousCall` argument is true if the listener is being invoked
245/// during the call to `addListener`. This can be useful if, for example,
246/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
247/// frame is requested if the call was asynchronous (after the current frame)
248/// and no rendering frame is requested if the call was synchronous (within the
249/// same stack frame as the call to [ImageStream.addListener]).
250typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
251
252/// Signature for listening to [ImageChunkEvent] events.
253///
254/// Used in [ImageStreamListener].
255typedef ImageChunkListener = void Function(ImageChunkEvent event);
256
257/// Signature for reporting errors when resolving images.
258///
259/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
260/// [precacheImage], to report errors.
261typedef ImageErrorListener = void Function(Object exception, StackTrace? stackTrace);
262
263/// An immutable notification of image bytes that have been incrementally loaded.
264///
265/// Chunk events represent progress notifications while an image is being
266/// loaded (e.g. from disk or over the network).
267///
268/// See also:
269///
270/// * [ImageChunkListener], the means by which callers get notified of
271/// these events.
272@immutable
273class ImageChunkEvent with Diagnosticable {
274 /// Creates a new chunk event.
275 const ImageChunkEvent({required this.cumulativeBytesLoaded, required this.expectedTotalBytes})
276 : assert(cumulativeBytesLoaded >= 0),
277 assert(expectedTotalBytes == null || expectedTotalBytes >= 0);
278
279 /// The number of bytes that have been received across the wire thus far.
280 final int cumulativeBytesLoaded;
281
282 /// The expected number of bytes that need to be received to finish loading
283 /// the image.
284 ///
285 /// This value is not necessarily equal to the expected _size_ of the image
286 /// in bytes, as the bytes required to load the image may be compressed.
287 ///
288 /// This value will be null if the number is not known in advance.
289 ///
290 /// When this value is null, the chunk event may still be useful as an
291 /// indication that data is loading (and how much), but it cannot represent a
292 /// loading completion percentage.
293 final int? expectedTotalBytes;
294
295 @override
296 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
297 super.debugFillProperties(properties);
298 properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
299 properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
300 }
301}
302
303/// A handle to an image resource.
304///
305/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
306/// (together represented by an [ImageInfo] object). The underlying image object
307/// might change over time, either because the image is animating or because the
308/// underlying image resource was mutated.
309///
310/// ImageStream objects can also represent an image that hasn't finished
311/// loading.
312///
313/// ImageStream objects are backed by [ImageStreamCompleter] objects.
314///
315/// The [ImageCache] will consider an image to be live until the listener count
316/// drops to zero after adding at least one listener. The
317/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
318/// tracking this information.
319///
320/// See also:
321///
322/// * [ImageProvider], which has an example that includes the use of an
323/// [ImageStream] in a [Widget].
324class ImageStream with Diagnosticable {
325 /// Create an initially unbound image stream.
326 ///
327 /// Once an [ImageStreamCompleter] is available, call [setCompleter].
328 ImageStream();
329
330 /// The completer that has been assigned to this image stream.
331 ///
332 /// Generally there is no need to deal with the completer directly.
333 ImageStreamCompleter? get completer => _completer;
334 ImageStreamCompleter? _completer;
335
336 List<ImageStreamListener>? _listeners;
337
338 /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
339 ///
340 /// This is usually done automatically by the [ImageProvider] that created the
341 /// [ImageStream].
342 ///
343 /// This method can only be called once per stream. To have an [ImageStream]
344 /// represent multiple images over time, assign it a completer that
345 /// completes several images in succession.
346 void setCompleter(ImageStreamCompleter value) {
347 assert(_completer == null);
348 _completer = value;
349 if (_listeners != null) {
350 final List<ImageStreamListener> initialListeners = _listeners!;
351 _listeners = null;
352 _completer!._addingInitialListeners = true;
353 initialListeners.forEach(_completer!.addListener);
354 _completer!._addingInitialListeners = false;
355 }
356 }
357
358 /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
359 /// object is available. If a concrete image is already available, this object
360 /// will call the listener synchronously.
361 ///
362 /// If the assigned [completer] completes multiple images over its lifetime,
363 /// this listener will fire multiple times.
364 ///
365 /// {@template flutter.painting.imageStream.addListener}
366 /// The listener will be passed a flag indicating whether a synchronous call
367 /// occurred. If the listener is added within a render object paint function,
368 /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
369 /// a paint.
370 ///
371 /// If a duplicate `listener` is registered N times, then it will be called N
372 /// times when the image stream completes (whether because a new image is
373 /// available or because an error occurs). Likewise, to remove all instances
374 /// of the listener, [removeListener] would need to called N times as well.
375 ///
376 /// When a `listener` receives an [ImageInfo] object, the `listener` is
377 /// responsible for disposing of the [ImageInfo.image].
378 /// {@endtemplate}
379 void addListener(ImageStreamListener listener) {
380 if (_completer != null) {
381 return _completer!.addListener(listener);
382 }
383 _listeners ??= <ImageStreamListener>[];
384 _listeners!.add(listener);
385 }
386
387 /// Stops listening for events from this stream's [ImageStreamCompleter].
388 ///
389 /// If [listener] has been added multiple times, this removes the _first_
390 /// instance of the listener.
391 void removeListener(ImageStreamListener listener) {
392 if (_completer != null) {
393 return _completer!.removeListener(listener);
394 }
395 assert(_listeners != null);
396 for (int i = 0; i < _listeners!.length; i += 1) {
397 if (_listeners![i] == listener) {
398 _listeners!.removeAt(i);
399 break;
400 }
401 }
402 }
403
404 /// Returns an object which can be used with `==` to determine if this
405 /// [ImageStream] shares the same listeners list as another [ImageStream].
406 ///
407 /// This can be used to avoid un-registering and re-registering listeners
408 /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
409 /// [ImageProvider].
410 ///
411 /// The key may change once in the lifetime of the object. When it changes, it
412 /// will go from being different than other [ImageStream]'s keys to
413 /// potentially being the same as others'. No notification is sent when this
414 /// happens.
415 Object get key => _completer ?? this;
416
417 @override
418 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
419 super.debugFillProperties(properties);
420 properties.add(
421 ObjectFlagProperty<ImageStreamCompleter>(
422 'completer',
423 _completer,
424 ifPresent: _completer?.toStringShort(),
425 ifNull: 'unresolved',
426 ),
427 );
428 properties.add(
429 ObjectFlagProperty<List<ImageStreamListener>>(
430 'listeners',
431 _listeners,
432 ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s"}',
433 ifNull: 'no listeners',
434 level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
435 ),
436 );
437 _completer?.debugFillProperties(properties);
438 }
439}
440
441/// An opaque handle that keeps an [ImageStreamCompleter] alive even if it has
442/// lost its last listener.
443///
444/// To create a handle, use [ImageStreamCompleter.keepAlive].
445///
446/// Such handles are useful when an image cache needs to keep a completer alive
447/// but does not actually have a listener subscribed, or when a widget that
448/// displays an image needs to temporarily unsubscribe from the completer but
449/// may re-subscribe in the future, for example when the [TickerMode] changes.
450class ImageStreamCompleterHandle {
451 ImageStreamCompleterHandle._(ImageStreamCompleter this._completer) {
452 _completer!._keepAliveHandles += 1;
453 assert(debugMaybeDispatchCreated('painting', 'ImageStreamCompleterHandle', this));
454 }
455
456 ImageStreamCompleter? _completer;
457
458 /// Call this method to signal the [ImageStreamCompleter] that it can now be
459 /// disposed when its last listener drops.
460 ///
461 /// This method must only be called once per object.
462 void dispose() {
463 assert(_completer != null);
464 assert(_completer!._keepAliveHandles > 0);
465 assert(!_completer!._disposed);
466
467 _completer!._keepAliveHandles -= 1;
468 _completer!._maybeDispose();
469 _completer = null;
470 assert(debugMaybeDispatchDisposed(this));
471 }
472}
473
474/// Base class for those that manage the loading of [dart:ui.Image] objects for
475/// [ImageStream]s.
476///
477/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
478/// [ImageProvider] subclass will return an [ImageStream] and automatically
479/// configure it with the right [ImageStreamCompleter] when possible.
480abstract class ImageStreamCompleter with Diagnosticable {
481 final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
482 final List<ImageErrorListener> _ephemeralErrorListeners = <ImageErrorListener>[];
483 ImageInfo? _currentImage;
484 FlutterErrorDetails? _currentError;
485
486 /// A string identifying the source of the underlying image.
487 String? debugLabel;
488
489 /// Whether any listeners are currently registered.
490 ///
491 /// Clients should not depend on this value for their behavior, because having
492 /// one listener's logic change when another listener happens to start or stop
493 /// listening will lead to extremely hard-to-track bugs. Subclasses might use
494 /// this information to determine whether to do any work when there are no
495 /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
496 /// to determine when to iterate through frames of an animated image.
497 ///
498 /// Typically this is used by overriding [addListener], checking if
499 /// [hasListeners] is false before calling `super.addListener()`, and if so,
500 /// starting whatever work is needed to determine when to notify listeners;
501 /// and similarly, by overriding [removeListener], checking if [hasListeners]
502 /// is false after calling `super.removeListener()`, and if so, stopping that
503 /// same work.
504 ///
505 /// The ephemeral error listeners (added through [addEphemeralErrorListener])
506 /// will not be taken into consideration in this property.
507 @protected
508 @visibleForTesting
509 bool get hasListeners => _listeners.isNotEmpty;
510
511 /// Whether the future listeners added to this completer are initial listeners.
512 ///
513 /// This can be set to true when an [ImageStream] adds its initial listeners to
514 /// this completer. This ultimately controls the synchronousCall parameter for
515 /// the listener callbacks. When adding cached listeners to a completer,
516 /// [_addingInitialListeners] can be set to false to indicate to the listeners
517 /// that they are being called asynchronously.
518 bool _addingInitialListeners = false;
519
520 /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
521 /// object is available or an error is reported. If a concrete image is
522 /// already available, or if an error has been already reported, this object
523 /// will notify the listener synchronously.
524 ///
525 /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
526 /// this listener's [ImageStreamListener.onImage] will fire multiple times.
527 ///
528 /// {@macro flutter.painting.imageStream.addListener}
529 ///
530 /// See also:
531 ///
532 /// * [addEphemeralErrorListener], which adds an error listener that is
533 /// automatically removed after first image load or error.
534 void addListener(ImageStreamListener listener) {
535 _checkDisposed();
536 _listeners.add(listener);
537 if (_currentImage != null) {
538 try {
539 listener.onImage(_currentImage!.clone(), !_addingInitialListeners);
540 } catch (exception, stack) {
541 reportError(
542 context: ErrorDescription('by a synchronously-called image listener'),
543 exception: exception,
544 stack: stack,
545 );
546 }
547 }
548 if (_currentError != null && listener.onError != null) {
549 try {
550 listener.onError!(_currentError!.exception, _currentError!.stack);
551 } catch (newException, newStack) {
552 if (newException != _currentError!.exception) {
553 FlutterError.reportError(
554 FlutterErrorDetails(
555 exception: newException,
556 library: 'image resource service',
557 context: ErrorDescription('by a synchronously-called image error listener'),
558 stack: newStack,
559 ),
560 );
561 }
562 }
563 }
564 }
565
566 /// Adds an error listener callback that is called when the first error is reported.
567 ///
568 /// The callback will be removed automatically after the first successful
569 /// image load or the first error - that is why it is called "ephemeral".
570 ///
571 /// If a concrete image is already available, the listener will be discarded
572 /// synchronously. If an error has been already reported, the listener
573 /// will be notified synchronously.
574 ///
575 /// The presence of a listener will affect neither the lifecycle of this object
576 /// nor what [hasListeners] reports.
577 ///
578 /// It is different from [addListener] in a few points: Firstly, this one only
579 /// listens to errors, while [addListener] listens to all kinds of events.
580 /// Secondly, this listener will be automatically removed according to the
581 /// rules mentioned above, while [addListener] will need manual removal.
582 /// Thirdly, this listener will not affect how this object is disposed, while
583 /// any non-removed listener added via [addListener] will forbid this object
584 /// from disposal.
585 ///
586 /// When you want to know full information and full control, use [addListener].
587 /// When you only want to get notified for an error ephemerally, use this function.
588 ///
589 /// See also:
590 ///
591 /// * [addListener], which adds a full-featured listener and needs manual
592 /// removal.
593 void addEphemeralErrorListener(ImageErrorListener listener) {
594 _checkDisposed();
595 if (_currentError != null) {
596 // immediately fire the listener, and no need to add to _ephemeralErrorListeners
597 try {
598 listener(_currentError!.exception, _currentError!.stack);
599 } catch (newException, newStack) {
600 if (newException != _currentError!.exception) {
601 FlutterError.reportError(
602 FlutterErrorDetails(
603 exception: newException,
604 library: 'image resource service',
605 context: ErrorDescription('by a synchronously-called image error listener'),
606 stack: newStack,
607 ),
608 );
609 }
610 }
611 } else if (_currentImage == null) {
612 // add to _ephemeralErrorListeners to wait for the error,
613 // only if no image has been loaded
614 _ephemeralErrorListeners.add(listener);
615 }
616 }
617
618 int _keepAliveHandles = 0;
619
620 /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from
621 /// being disposed at least until the handle is disposed.
622 ///
623 /// Such handles are useful when an image cache needs to keep a completer
624 /// alive but does not itself have a listener subscribed, or when a widget
625 /// that displays an image needs to temporarily unsubscribe from the completer
626 /// but may re-subscribe in the future, for example when the [TickerMode]
627 /// changes.
628 ImageStreamCompleterHandle keepAlive() {
629 _checkDisposed();
630 return ImageStreamCompleterHandle._(this);
631 }
632
633 /// Stops the specified [listener] from receiving image stream events.
634 ///
635 /// If [listener] has been added multiple times, this removes the _first_
636 /// instance of the listener.
637 ///
638 /// Once all listeners have been removed and all [keepAlive] handles have been
639 /// disposed, this image stream is no longer usable.
640 void removeListener(ImageStreamListener listener) {
641 _checkDisposed();
642 for (int i = 0; i < _listeners.length; i += 1) {
643 if (_listeners[i] == listener) {
644 _listeners.removeAt(i);
645 break;
646 }
647 }
648 if (_listeners.isEmpty) {
649 final List<VoidCallback> callbacks = _onLastListenerRemovedCallbacks.toList();
650 for (final VoidCallback callback in callbacks) {
651 callback();
652 }
653 _onLastListenerRemovedCallbacks.clear();
654 _maybeDispose();
655 }
656 }
657
658 bool _disposed = false;
659
660 /// Called when this [ImageStreamCompleter] has actually been disposed.
661 ///
662 /// Subclasses should override this if they need to clean up resources when
663 /// they are disposed.
664 @mustCallSuper
665 @protected
666 void onDisposed() {}
667
668 /// Disposes this [ImageStreamCompleter] unless:
669 ///
670 /// 1. It is already disposed
671 /// 2. It has listeners.
672 /// 3. It has active "keep alive" handles.
673 @nonVirtual
674 void maybeDispose() {
675 _maybeDispose();
676 }
677
678 @mustCallSuper
679 void _maybeDispose() {
680 if (_disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
681 return;
682 }
683
684 _ephemeralErrorListeners.clear();
685 _currentImage?.dispose();
686 _currentImage = null;
687 _disposed = true;
688 onDisposed();
689 }
690
691 void _checkDisposed() {
692 if (_disposed) {
693 throw StateError(
694 'Stream has been disposed.\n'
695 'An ImageStream is considered disposed once at least one listener has '
696 'been added and subsequently all listeners have been removed and no '
697 'handles are outstanding from the keepAlive method.\n'
698 'To resolve this error, maintain at least one listener on the stream, '
699 'or create an ImageStreamCompleterHandle from the keepAlive '
700 'method, or create a new stream for the image.',
701 );
702 }
703 }
704
705 final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];
706
707 /// Adds a callback to call when [removeListener] results in an empty
708 /// list of listeners and there are no [keepAlive] handles outstanding.
709 ///
710 /// This callback will never fire if [removeListener] is never called.
711 void addOnLastListenerRemovedCallback(VoidCallback callback) {
712 _checkDisposed();
713 _onLastListenerRemovedCallbacks.add(callback);
714 }
715
716 /// Removes a callback previously supplied to
717 /// [addOnLastListenerRemovedCallback].
718 void removeOnLastListenerRemovedCallback(VoidCallback callback) {
719 _checkDisposed();
720 _onLastListenerRemovedCallbacks.remove(callback);
721 }
722
723 /// Calls all the registered listeners to notify them of a new image.
724 @protected
725 @pragma('vm:notify-debugger-on-exception')
726 void setImage(ImageInfo image) {
727 _checkDisposed();
728 _currentImage?.dispose();
729 _currentImage = image;
730
731 _ephemeralErrorListeners.clear();
732
733 if (_listeners.isEmpty) {
734 return;
735 }
736 // Make a copy to allow for concurrent modification.
737 final List<ImageStreamListener> localListeners = List<ImageStreamListener>.of(_listeners);
738 for (final ImageStreamListener listener in localListeners) {
739 try {
740 listener.onImage(image.clone(), false);
741 } catch (exception, stack) {
742 reportError(
743 context: ErrorDescription('by an image listener'),
744 exception: exception,
745 stack: stack,
746 );
747 }
748 }
749 }
750
751 /// Calls all the registered error listeners to notify them of an error that
752 /// occurred while resolving the image.
753 ///
754 /// If no error listeners (listeners with an [ImageStreamListener.onError]
755 /// specified) are attached, or if the handlers all rethrow the exception
756 /// verbatim (with `throw exception`), a [FlutterError] will be reported using
757 /// [FlutterError.reportError].
758 ///
759 /// The `context` should be a string describing where the error was caught, in
760 /// a form that will make sense in English when following the word "thrown",
761 /// as in "thrown while obtaining the image from the network" (for the context
762 /// "while obtaining the image from the network").
763 ///
764 /// The `exception` is the error being reported; the `stack` is the
765 /// [StackTrace] associated with the exception.
766 ///
767 /// The `informationCollector` is a callback (of type [InformationCollector])
768 /// that is called when the exception is used by [FlutterError.reportError].
769 /// It is used to obtain further details to include in the logs, which may be
770 /// expensive to collect, and thus should only be collected if the error is to
771 /// be logged in the first place.
772 ///
773 /// The `silent` argument causes the exception to not be reported to the logs
774 /// in release builds, if passed to [FlutterError.reportError]. (It is still
775 /// sent to error handlers.) It should be set to true if the error is one that
776 /// is expected to be encountered in release builds, for example network
777 /// errors. That way, logs on end-user devices will not have spurious
778 /// messages, but errors during development will still be reported.
779 ///
780 /// See [FlutterErrorDetails] for further details on these values.
781 @pragma('vm:notify-debugger-on-exception')
782 void reportError({
783 DiagnosticsNode? context,
784 required Object exception,
785 StackTrace? stack,
786 InformationCollector? informationCollector,
787 bool silent = false,
788 }) {
789 _currentError = FlutterErrorDetails(
790 exception: exception,
791 stack: stack,
792 library: 'image resource service',
793 context: context,
794 informationCollector: informationCollector,
795 silent: silent,
796 );
797
798 // Make a copy to allow for concurrent modification.
799 final List<ImageErrorListener> localErrorListeners = <ImageErrorListener>[
800 ..._listeners
801 .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
802 .whereType<ImageErrorListener>(),
803 ..._ephemeralErrorListeners,
804 ];
805
806 _ephemeralErrorListeners.clear();
807
808 bool handled = false;
809 for (final ImageErrorListener errorListener in localErrorListeners) {
810 try {
811 errorListener(exception, stack);
812 handled = true;
813 } catch (newException, newStack) {
814 if (newException != exception) {
815 FlutterError.reportError(
816 FlutterErrorDetails(
817 context: ErrorDescription('when reporting an error to an image listener'),
818 library: 'image resource service',
819 exception: newException,
820 stack: newStack,
821 ),
822 );
823 }
824 }
825 }
826 if (!handled) {
827 FlutterError.reportError(_currentError!);
828 }
829 }
830
831 /// Calls all the registered [ImageChunkListener]s (listeners with an
832 /// [ImageStreamListener.onChunk] specified) to notify them of a new
833 /// [ImageChunkEvent].
834 @protected
835 void reportImageChunkEvent(ImageChunkEvent event) {
836 _checkDisposed();
837 if (hasListeners) {
838 // Make a copy to allow for concurrent modification.
839 final List<ImageChunkListener> localListeners = _listeners
840 .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
841 .whereType<ImageChunkListener>()
842 .toList();
843 for (final ImageChunkListener listener in localListeners) {
844 listener(event);
845 }
846 }
847 }
848
849 /// Accumulates a list of strings describing the object's state. Subclasses
850 /// should override this to have their information included in [toString].
851 @override
852 void debugFillProperties(DiagnosticPropertiesBuilder description) {
853 super.debugFillProperties(description);
854 description.add(
855 DiagnosticsProperty<ImageInfo>(
856 'current',
857 _currentImage,
858 ifNull: 'unresolved',
859 showName: false,
860 ),
861 );
862 description.add(
863 ObjectFlagProperty<List<ImageStreamListener>>(
864 'listeners',
865 _listeners,
866 ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s"}',
867 ),
868 );
869 description.add(
870 ObjectFlagProperty<List<ImageErrorListener>>(
871 'ephemeralErrorListeners',
872 _ephemeralErrorListeners,
873 ifPresent:
874 '${_ephemeralErrorListeners.length} ephemeralErrorListener${_ephemeralErrorListeners.length == 1 ? "" : "s"}',
875 ),
876 );
877 description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
878 }
879}
880
881/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
882/// with only one frame).
883class OneFrameImageStreamCompleter extends ImageStreamCompleter {
884 /// Creates a manager for one-frame [ImageStream]s.
885 ///
886 /// The image resource awaits the given [Future]. When the future resolves,
887 /// it notifies the [ImageListener]s that have been registered with
888 /// [addListener].
889 ///
890 /// The [InformationCollector], if provided, is invoked if the given [Future]
891 /// resolves with an error, and can be used to supplement the reported error
892 /// message (for example, giving the image's URL).
893 ///
894 /// Errors are reported using [FlutterError.reportError] with the `silent`
895 /// argument on [FlutterErrorDetails] set to true, meaning that by default the
896 /// message is only dumped to the console in debug mode (see [
897 /// FlutterErrorDetails]).
898 OneFrameImageStreamCompleter(
899 Future<ImageInfo> image, {
900 InformationCollector? informationCollector,
901 }) {
902 image.then<void>(
903 setImage,
904 onError: (Object error, StackTrace stack) {
905 reportError(
906 context: ErrorDescription('resolving a single-frame image stream'),
907 exception: error,
908 stack: stack,
909 informationCollector: informationCollector,
910 silent: true,
911 );
912 },
913 );
914 }
915}
916
917/// Manages the decoding and scheduling of image frames.
918///
919/// New frames will only be emitted while there are registered listeners to the
920/// stream (registered with [addListener]).
921///
922/// This class deals with 2 types of frames:
923///
924/// * image frames - image frames of an animated image.
925/// * app frames - frames that the flutter engine is drawing to the screen to
926/// show the app GUI.
927///
928/// For single frame images the stream will only complete once.
929///
930/// For animated images, this class eagerly decodes the next image frame,
931/// and notifies the listeners that a new frame is ready on the first app frame
932/// that is scheduled after the image frame duration has passed.
933///
934/// Scheduling new timers only from scheduled app frames, makes sure we pause
935/// the animation when the app is not visible (as new app frames will not be
936/// scheduled).
937///
938/// See the following timeline example:
939///
940/// | Time | Event | Comment |
941/// |------|--------------------------------------------|---------------------------|
942/// | t1 | App frame scheduled (image frame A posted) | |
943/// | t2 | App frame scheduled | |
944/// | t3 | App frame scheduled | |
945/// | t4 | Image frame B decoded | |
946/// | t5 | App frame scheduled | t5 - t1 < frameB_duration |
947/// | t6 | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
948///
949class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
950 /// Creates a image stream completer.
951 ///
952 /// Immediately starts decoding the first image frame when the codec is ready.
953 ///
954 /// The `codec` parameter is a future for an initialized [ui.Codec] that will
955 /// be used to decode the image. This completer takes ownership of the passed
956 /// `codec` and will dispose it once it is no longer needed.
957 ///
958 /// The `scale` parameter is the linear scale factor for drawing this frames
959 /// of this image at their intended size.
960 ///
961 /// The `tag` parameter is passed on to created [ImageInfo] objects to
962 /// help identify the source of the image.
963 ///
964 /// The `chunkEvents` parameter is an optional stream of notifications about
965 /// the loading progress of the image. If this stream is provided, the events
966 /// produced by the stream will be delivered to registered [ImageChunkListener]s
967 /// (see [addListener]).
968 MultiFrameImageStreamCompleter({
969 required Future<ui.Codec> codec,
970 required double scale,
971 String? debugLabel,
972 Stream<ImageChunkEvent>? chunkEvents,
973 InformationCollector? informationCollector,
974 }) : _informationCollector = informationCollector,
975 _scale = scale {
976 this.debugLabel = debugLabel;
977 codec.then<void>(
978 _handleCodecReady,
979 onError: (Object error, StackTrace stack) {
980 reportError(
981 context: ErrorDescription('resolving an image codec'),
982 exception: error,
983 stack: stack,
984 informationCollector: informationCollector,
985 silent: true,
986 );
987 },
988 );
989 if (chunkEvents != null) {
990 _chunkSubscription = chunkEvents.listen(
991 reportImageChunkEvent,
992 onError: (Object error, StackTrace stack) {
993 reportError(
994 context: ErrorDescription('loading an image'),
995 exception: error,
996 stack: stack,
997 informationCollector: informationCollector,
998 silent: true,
999 );
1000 },
1001 );
1002 }
1003 }
1004
1005 StreamSubscription<ImageChunkEvent>? _chunkSubscription;
1006 ui.Codec? _codec;
1007 final double _scale;
1008 final InformationCollector? _informationCollector;
1009 ui.FrameInfo? _nextFrame;
1010 // When the current was first shown.
1011 late Duration _shownTimestamp;
1012 // The requested duration for the current frame;
1013 Duration? _frameDuration;
1014 // How many frames have been emitted so far.
1015 int _framesEmitted = 0;
1016 Timer? _timer;
1017
1018 // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
1019 bool _frameCallbackScheduled = false;
1020
1021 void _handleCodecReady(ui.Codec codec) {
1022 _codec = codec;
1023 assert(_codec != null);
1024
1025 if (hasListeners) {
1026 _decodeNextFrameAndSchedule();
1027 }
1028 }
1029
1030 void _handleAppFrame(Duration timestamp) {
1031 _frameCallbackScheduled = false;
1032 if (!hasListeners) {
1033 return;
1034 }
1035 assert(_nextFrame != null);
1036 if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
1037 _emitFrame(
1038 ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel),
1039 );
1040 _shownTimestamp = timestamp;
1041 _frameDuration = _nextFrame!.duration;
1042 _nextFrame!.image.dispose();
1043 _nextFrame = null;
1044 if (_codec == null) {
1045 // codec was disposed during _emitFrame
1046 return;
1047 }
1048 final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
1049 if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
1050 _decodeNextFrameAndSchedule();
1051 return;
1052 }
1053
1054 _codec!.dispose();
1055 _codec = null;
1056 return;
1057 }
1058 final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
1059 _timer = Timer(delay * timeDilation, () {
1060 _scheduleAppFrame();
1061 });
1062 }
1063
1064 bool _isFirstFrame() {
1065 return _frameDuration == null;
1066 }
1067
1068 bool _hasFrameDurationPassed(Duration timestamp) {
1069 return timestamp - _shownTimestamp >= _frameDuration!;
1070 }
1071
1072 Future<void> _decodeNextFrameAndSchedule() async {
1073 // This will be null if we gave it away. If not, it's still ours and it
1074 // must be disposed of.
1075 _nextFrame?.image.dispose();
1076 _nextFrame = null;
1077 try {
1078 _nextFrame = await _codec!.getNextFrame();
1079 } catch (exception, stack) {
1080 reportError(
1081 context: ErrorDescription('resolving an image frame'),
1082 exception: exception,
1083 stack: stack,
1084 informationCollector: _informationCollector,
1085 silent: true,
1086 );
1087 return;
1088 }
1089 if (_codec == null) {
1090 // codec was disposed during getNextFrame
1091 return;
1092 }
1093
1094 if (_codec!.frameCount == 1) {
1095 // ImageStreamCompleter listeners removed while waiting for next frame to
1096 // be decoded.
1097 // There's no reason to emit the frame without active listeners.
1098 if (!hasListeners) {
1099 return;
1100 }
1101 // This is not an animated image, just return it and don't schedule more
1102 // frames.
1103 _emitFrame(
1104 ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel),
1105 );
1106 _nextFrame!.image.dispose();
1107 _nextFrame = null;
1108
1109 _codec?.dispose();
1110 _codec = null;
1111 return;
1112 }
1113 _scheduleAppFrame();
1114 }
1115
1116 void _scheduleAppFrame() {
1117 if (_frameCallbackScheduled) {
1118 return;
1119 }
1120 _frameCallbackScheduled = true;
1121 SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
1122 }
1123
1124 void _emitFrame(ImageInfo imageInfo) {
1125 setImage(imageInfo);
1126 _framesEmitted += 1;
1127 }
1128
1129 @override
1130 void addListener(ImageStreamListener listener) {
1131 if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) {
1132 _decodeNextFrameAndSchedule();
1133 }
1134 super.addListener(listener);
1135 }
1136
1137 @override
1138 void removeListener(ImageStreamListener listener) {
1139 super.removeListener(listener);
1140 if (!hasListeners) {
1141 _timer?.cancel();
1142 _timer = null;
1143 }
1144 }
1145
1146 @override
1147 void _maybeDispose() {
1148 super._maybeDispose();
1149 if (_disposed) {
1150 _chunkSubscription?.onData(null);
1151 _chunkSubscription?.cancel();
1152 _chunkSubscription = null;
1153
1154 _codec?.dispose();
1155 _codec = null;
1156 }
1157 }
1158}
1159