Unverified Commit d5d47f63 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

[H] Add ImageStreamCompleter.hasListeners (and cleanup) (#25865)

* Remove stray extra space

* Add ImageStreamCompleter.hasListeners (and cleanup)

This is mostly just some cleanup of stuff I ran into, but it makes
`hasListeners` protected on `ImageStreamCompleter`, because otherwise
there's no way to track if listeners are registered or not.

* Address review comments
parent 88fc38cf
...@@ -59,6 +59,11 @@ class FlutterErrorDetails { ...@@ -59,6 +59,11 @@ class FlutterErrorDetails {
/// A human-readable description of where the error was caught (as opposed to /// A human-readable description of where the error was caught (as opposed to
/// where it was thrown). /// where it was thrown).
///
/// The string should be in a form that will make sense in English when
/// following the word "thrown", as in "thrown while obtaining the image from
/// the network" (for the context "while obtaining the image from the
/// network").
final String context; final String context;
/// A callback which filters the [stack] trace. Receives an iterable of /// A callback which filters the [stack] trace. Receives an iterable of
......
...@@ -2268,7 +2268,7 @@ class InputDecoration { ...@@ -2268,7 +2268,7 @@ class InputDecoration {
/// can be expanded beyond that. Anything larger than 24px will require /// can be expanded beyond that. Anything larger than 24px will require
/// additional padding to ensure it matches the material spec of 12px padding /// additional padding to ensure it matches the material spec of 12px padding
/// between the right edge of the input and trailing edge of the prefix icon. /// between the right edge of the input and trailing edge of the prefix icon.
/// The following snipped shows how to pad the trailing edge of the suffix /// The following snippet shows how to pad the trailing edge of the suffix
/// icon: /// icon:
/// ///
/// ```dart /// ```dart
......
...@@ -61,7 +61,7 @@ class ImageInfo { ...@@ -61,7 +61,7 @@ class ImageInfo {
/// Used by [ImageStream]. /// Used by [ImageStream].
/// ///
/// The `synchronousCall` argument is true if the listener is being invoked /// The `synchronousCall` argument is true if the listener is being invoked
/// during the call to addListener. This can be useful if, for example, /// during the call to `addListener`. This can be useful if, for example,
/// [ImageStream.addListener] is invoked during a frame, so that a new rendering /// [ImageStream.addListener] is invoked during a frame, so that a new rendering
/// frame is requested if the call was asynchronous (after the current frame) /// frame is requested if the call was asynchronous (after the current frame)
/// and no rendering frame is requested if the call was synchronous (within the /// and no rendering frame is requested if the call was synchronous (within the
...@@ -148,9 +148,13 @@ class ImageStream extends Diagnosticable { ...@@ -148,9 +148,13 @@ class ImageStream extends Diagnosticable {
/// `listener`. If an error occurred, `onError` will be called instead of /// `listener`. If an error occurred, `onError` will be called instead of
/// `listener`. /// `listener`.
/// ///
/// Many `listener`s can have the same `onError` and one `listener` can also /// If a `listener` or `onError` handler is registered multiple times, then it
/// have multiple `onError` by invoking [addListener] multiple times with /// will be called multiple times when the image stream completes (whether
/// a different `onError` each time. /// because a new image is available or because an error occurs,
/// respectively). In general, registering a listener multiple times is
/// discouraged because [removeListener] will remove the first instance that
/// was added, even if it was added with a different `onError` than the
/// intended paired `addListener` call.
void addListener(ImageListener listener, { ImageErrorListener onError }) { void addListener(ImageListener listener, { ImageErrorListener onError }) {
if (_completer != null) if (_completer != null)
return _completer.addListener(listener, onError: onError); return _completer.addListener(listener, onError: onError);
...@@ -160,11 +164,26 @@ class ImageStream extends Diagnosticable { ...@@ -160,11 +164,26 @@ class ImageStream extends Diagnosticable {
/// Stop listening for new concrete [ImageInfo] objects and errors from /// Stop listening for new concrete [ImageInfo] objects and errors from
/// the `listener`'s associated [ImageErrorListener]. /// the `listener`'s associated [ImageErrorListener].
///
/// If `listener` has been added multiple times, this removes the first
/// instance of the listener, along with the `onError` listener that was
/// registered with that first instance. This might not be the instance that
/// the `addListener` corresponding to this `removeListener` had added.
///
/// For example, if one widget calls [addListener] with a global static
/// function and a private error handler, and another widget calls
/// [addListener] with the same global static function but a different private
/// error handler, then the second widget is disposed and removes the image
/// listener (the aforementioned global static function), it will remove the
/// error handler from the first widget, not the second. If an error later
/// occurs, the first widget, which is still supposedly listening, will not
/// receive any messages, while the second, which is supposedly disposed, will
/// have its callback invoked.
void removeListener(ImageListener listener) { void removeListener(ImageListener listener) {
if (_completer != null) if (_completer != null)
return _completer.removeListener(listener); return _completer.removeListener(listener);
assert(_listeners != null); assert(_listeners != null);
for (int i = 0; i < _listeners.length; ++i) { for (int i = 0; i < _listeners.length; i += 1) {
if (_listeners[i].listener == listener) { if (_listeners[i].listener == listener) {
_listeners.removeAt(i); _listeners.removeAt(i);
break; break;
...@@ -216,6 +235,24 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -216,6 +235,24 @@ abstract class ImageStreamCompleter extends Diagnosticable {
ImageInfo _currentImage; ImageInfo _currentImage;
FlutterErrorDetails _currentError; FlutterErrorDetails _currentError;
/// Whether any listeners are currently registered.
///
/// Clients should not depend on this value for their behavior, because having
/// one listener's logic change when another listener happens to start or stop
/// listening will lead to extremely hard-to-track bugs. Subclasses might use
/// this information to determine whether to do any work when there are no
/// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
/// to determine when to iterate through frames of an animated image.
///
/// Typically this is used by overriding [addListener], checking if
/// [hasListeners] is false before calling `super.addListener()`, and if so,
/// starting whatever work is needed to determine when to call
/// [notifyListeners]; and similarly, by overriding [removeListener], checking
/// if [hasListeners] is false after calling `super.removeListener()`, and if
/// so, stopping that same work.
@protected
bool get hasListeners => _listeners.isNotEmpty;
/// Adds a listener callback that is called whenever a new concrete [ImageInfo] /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
/// object is available or an error is reported. If a concrete image is /// object is available or an error is reported. If a concrete image is
/// already available, or if an error has been already reported, this object /// already available, or if an error has been already reported, this object
...@@ -228,6 +265,18 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -228,6 +265,18 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// occurred. If the listener is added within a render object paint function, /// occurred. If the listener is added within a render object paint function,
/// then use this flag to avoid calling [RenderObject.markNeedsPaint] during /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
/// a paint. /// a paint.
///
/// An [ImageErrorListener] can also optionally be added along with the
/// `listener`. If an error occurred, `onError` will be called instead of
/// `listener`.
///
/// If a `listener` or `onError` handler is registered multiple times, then it
/// will be called multiple times when the image stream completes (whether
/// because a new image is available or because an error occurs,
/// respectively). In general, registering a listener multiple times is
/// discouraged because [removeListener] will remove the first instance that
/// was added, even if it was added with a different `onError` than the
/// intended paired `addListener` call.
void addListener(ImageListener listener, { ImageErrorListener onError }) { void addListener(ImageListener listener, { ImageErrorListener onError }) {
_listeners.add(_ImageListenerPair(listener, onError)); _listeners.add(_ImageListenerPair(listener, onError));
if (_currentImage != null) { if (_currentImage != null) {
...@@ -259,8 +308,13 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -259,8 +308,13 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// Stop listening for new concrete [ImageInfo] objects and errors from /// Stop listening for new concrete [ImageInfo] objects and errors from
/// its associated [ImageErrorListener]. /// its associated [ImageErrorListener].
///
/// If `listener` has been added multiple times, this removes the first
/// instance of the listener, along with the `onError` listener that was
/// registered with that first instance. This might not be the instance that
/// the `addListener` corresponding to this `removeListener` had added.
void removeListener(ImageListener listener) { void removeListener(ImageListener listener) {
for (int i = 0; i < _listeners.length; ++i) { for (int i = 0; i < _listeners.length; i += 1) {
if (_listeners[i].listener == listener) { if (_listeners[i].listener == listener) {
_listeners.removeAt(i); _listeners.removeAt(i);
break; break;
...@@ -295,6 +349,29 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -295,6 +349,29 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// ///
/// If no error listeners are attached, a [FlutterError] will be reported /// If no error listeners are attached, a [FlutterError] will be reported
/// instead. /// instead.
///
/// The `context` should be a string describing where the error was caught, in
/// a form that will make sense in English when following the word "thrown",
/// as in "thrown while obtaining the image from the network" (for the context
/// "while obtaining the image from the network").
///
/// The `exception` is the error being reported; the `stack` is the
/// [StackTrace] associated with the exception.
///
/// The `informationCollector` is a callback (of type [InformationCollector])
/// that is called when the exception is used by [FlutterError.reportError].
/// It is used to obtain further details to include in the logs, which may be
/// expensive to collect, and thus should only be collected if the error is to
/// be logged in the first place.
///
/// The `silent` argument causes the exception to not be reported to the logs
/// in release builds, if passed to [FlutterError.reportError]. (It is still
/// sent to error handlers.) It should be set to true if the error is one that
/// is expected to be encountered in release builds, for example network
/// errors. That way, logs on end-user devices will not have spurious
/// messages, but errors during development will still be reported.
///
/// See [FlutterErrorDetails] for further details on these values.
@protected @protected
void reportError({ void reportError({
String context, String context,
...@@ -328,7 +405,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -328,7 +405,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetails( FlutterErrorDetails(
context: 'by an image error listener', context: 'when reporting an error to an image listener',
library: 'image resource service', library: 'image resource service',
exception: exception, exception: exception,
stack: stack, stack: stack,
...@@ -465,7 +542,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -465,7 +542,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
} }
void _handleAppFrame(Duration timestamp) { void _handleAppFrame(Duration timestamp) {
if (!_hasActiveListeners) if (!hasListeners)
return; return;
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) { if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale)); _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
...@@ -520,20 +597,17 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -520,20 +597,17 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_framesEmitted += 1; _framesEmitted += 1;
} }
bool get _hasActiveListeners => _listeners.isNotEmpty;
@override @override
void addListener(ImageListener listener, { ImageErrorListener onError }) { void addListener(ImageListener listener, { ImageErrorListener onError }) {
if (!_hasActiveListeners && _codec != null) { if (!hasListeners && _codec != null)
_decodeNextFrameAndSchedule(); _decodeNextFrameAndSchedule();
}
super.addListener(listener, onError: onError); super.addListener(listener, onError: onError);
} }
@override @override
void removeListener(ImageListener listener) { void removeListener(ImageListener listener) {
super.removeListener(listener); super.removeListener(listener);
if (!_hasActiveListeners) { if (!hasListeners) {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
int yieldCount; int yieldCount;
......
// Copyright 2016 The Chromium Authors. All rights reserved. // Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment