Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 60 additions & 36 deletions packages/flutter/lib/src/widgets/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ abstract class StreamBuilderBase<T, S> extends StatefulWidget {
/// is combined with the new data item in the fold computation.
S afterData(S current, T data);

/// Returns an updated version of the [current] summary following an error.
/// Returns an updated version of the [current] summary following an error
/// with a stack trace.
///
/// The default implementation returns [current] as is.
S afterError(S current, Object error) => current;
S afterError(S current, Object error, StackTrace stackTrace) => current;

/// Returns an updated version of the [current] summary following stream
/// termination.
Expand Down Expand Up @@ -138,9 +139,9 @@ class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
setState(() {
_summary = widget.afterData(_summary, data);
});
}, onError: (Object error) {
}, onError: (Object error, StackTrace stackTrace) {
setState(() {
_summary = widget.afterError(_summary, error);
_summary = widget.afterError(_summary, error, stackTrace);
});
}, onDone: () {
setState(() {
Expand Down Expand Up @@ -204,22 +205,31 @@ enum ConnectionState {
@immutable
class AsyncSnapshot<T> {
/// Creates an [AsyncSnapshot] with the specified [connectionState],
/// and optionally either [data] or [error] (but not both).
const AsyncSnapshot._(this.connectionState, this.data, this.error)
/// and optionally either [data] or [error] with an optional [stackTrace]
/// (but not both data and error).
const AsyncSnapshot._(this.connectionState, this.data, this.error, this.stackTrace)
: assert(connectionState != null),
assert(!(data != null && error != null));
assert(!(data != null && error != null)),
assert(stackTrace == null || error != null);

/// Creates an [AsyncSnapshot] in [ConnectionState.none] with null data and error.
const AsyncSnapshot.nothing() : this._(ConnectionState.none, null, null);
const AsyncSnapshot.nothing() : this._(ConnectionState.none, null, null, null);

/// Creates an [AsyncSnapshot] in [ConnectionState.waiting] with null data and error.
const AsyncSnapshot.waiting() : this._(ConnectionState.waiting, null, null);
const AsyncSnapshot.waiting() : this._(ConnectionState.waiting, null, null, null);

/// Creates an [AsyncSnapshot] in the specified [state] and with the specified [data].
const AsyncSnapshot.withData(ConnectionState state, T data): this._(state, data, null);
const AsyncSnapshot.withData(ConnectionState state, T data): this._(state, data, null, null);

/// Creates an [AsyncSnapshot] in the specified [state] and with the specified [error].
const AsyncSnapshot.withError(ConnectionState state, Object error) : this._(state, null, error);
/// Creates an [AsyncSnapshot] in the specified [state] with the specified [error]
/// and a [stackTrace].
///
/// If no [stackTrace] is explicitly specified, [StackTrace.empty] will be used instead.
const AsyncSnapshot.withError(
ConnectionState state,
Object error, [
StackTrace stackTrace = StackTrace.empty,
]) : this._(state, null, error, stackTrace);

/// Current state of connection to the asynchronous computation.
final ConnectionState connectionState;
Expand Down Expand Up @@ -254,11 +264,20 @@ class AsyncSnapshot<T> {
/// If [data] is not null, this will be null.
final Object? error;

/// The latest stack trace object received by the asynchronous computation.
///
/// This will not be null iff [error] is not null. Consequently, [stackTrace]
/// will be non-null when [hasError] is true.
///
/// However, even when not null, [stackTrace] might be empty. The stack trace
/// is empty when there is an error but no stack trace has been provided.
final StackTrace? stackTrace;

/// Returns a snapshot like this one, but in the specified [state].
///
/// The [data] and [error] fields persist unmodified, even if the new state is
/// [ConnectionState.none].
AsyncSnapshot<T> inState(ConnectionState state) => AsyncSnapshot<T>._(state, data, error);
/// The [data], [error], and [stackTrace] fields persist unmodified, even if
/// the new state is [ConnectionState.none].
AsyncSnapshot<T> inState(ConnectionState state) => AsyncSnapshot<T>._(state, data, error, stackTrace);

/// Returns whether this snapshot contains a non-null [data] value.
///
Expand All @@ -275,7 +294,7 @@ class AsyncSnapshot<T> {
bool get hasError => error != null;

@override
String toString() => '${objectRuntimeType(this, 'AsyncSnapshot')}($connectionState, $data, $error)';
String toString() => '${objectRuntimeType(this, 'AsyncSnapshot')}($connectionState, $data, $error, $stackTrace)';

@override
bool operator ==(Object other) {
Expand All @@ -284,7 +303,8 @@ class AsyncSnapshot<T> {
return other is AsyncSnapshot<T>
&& other.connectionState == connectionState
&& other.data == data
&& other.error == error;
&& other.error == error
&& other.stackTrace == stackTrace;
}

@override
Expand Down Expand Up @@ -318,12 +338,12 @@ typedef AsyncWidgetBuilder<T> = Widget Function(BuildContext context, AsyncSnaps
/// of the following snapshots that includes the last one (the one with
/// ConnectionState.done):
///
/// * `new AsyncSnapshot<int>.withData(ConnectionState.waiting, null)`
Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch! This must be old.

Copy link
Contributor

Choose a reason for hiding this comment

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

we really should find a way to analyze this

/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 0)`
/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 1)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.waiting, null)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.active, 0)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.active, 1)`
/// * ...
/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 9)`
/// * `new AsyncSnapshot<int>.withData(ConnectionState.done, 9)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.active, 9)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.done, 9)`
///
/// The actual sequence of invocations of the [builder] depends on the relative
/// timing of events produced by the stream and the build rate of the Flutter
Expand All @@ -332,15 +352,15 @@ typedef AsyncWidgetBuilder<T> = Widget Function(BuildContext context, AsyncSnaps
/// Changing the [StreamBuilder] configuration to another stream during event
/// generation introduces snapshot pairs of the form:
///
/// * `new AsyncSnapshot<int>.withData(ConnectionState.none, 5)`
/// * `new AsyncSnapshot<int>.withData(ConnectionState.waiting, 5)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.none, 5)`
/// * `AsyncSnapshot<int>.withData(ConnectionState.waiting, 5)`
///
/// The latter will be produced only when the new stream is non-null, and the
/// former only when the old stream is non-null.
///
/// The stream may produce errors, resulting in snapshots of the form:
///
/// * `new AsyncSnapshot<int>.withError(ConnectionState.active, 'some error')`
/// * `AsyncSnapshot<int>.withError(ConnectionState.active, 'some error', someStackTrace)`
///
/// The data and error fields of snapshots produced are only changed when the
/// state is `ConnectionState.active`.
Expand Down Expand Up @@ -386,7 +406,11 @@ typedef AsyncWidgetBuilder<T> = Widget Function(BuildContext context, AsyncSnaps
/// Padding(
/// padding: const EdgeInsets.only(top: 16),
/// child: Text('Error: ${snapshot.error}'),
/// )
/// ),
/// Padding(
/// padding: const EdgeInsets.only(top: 8),
/// child: Text('Stack trace: ${snapshot.stackTrace}'),
/// ),
/// ];
/// } else {
/// switch (snapshot.connectionState) {
Expand Down Expand Up @@ -511,8 +535,8 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
}

@override
AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error) {
return AsyncSnapshot<T>.withError(ConnectionState.active, error);
AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error, StackTrace stackTrace) {
return AsyncSnapshot<T>.withError(ConnectionState.active, error, stackTrace);
}

@override
Expand Down Expand Up @@ -559,14 +583,14 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
/// is null, the [builder] will be called with either both or only the latter of
/// the following snapshots:
///
/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
/// * `new AsyncSnapshot<String>.withData(ConnectionState.done, 'some data')`
/// * `AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
/// * `AsyncSnapshot<String>.withData(ConnectionState.done, 'some data')`
///
/// If that same future instead completed with an error, the [builder] would be
/// called with either both or only the latter of:
///
/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
/// * `new AsyncSnapshot<String>.withError(ConnectionState.done, 'some error')`
/// * `AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
/// * `AsyncSnapshot<String>.withError(ConnectionState.done, 'some error', someStackTrace)`
///
/// The initial snapshot data can be controlled by specifying [initialData]. You
/// would use this facility to ensure that if the [builder] is invoked before
Expand All @@ -579,8 +603,8 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
/// old future has already completed successfully with data as above, changing
/// configuration to a new future results in snapshot pairs of the form:
///
/// * `new AsyncSnapshot<String>.withData(ConnectionState.none, 'data of first future')`
/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, 'data of second future')`
/// * `AsyncSnapshot<String>.withData(ConnectionState.none, 'data of first future')`
/// * `AsyncSnapshot<String>.withData(ConnectionState.waiting, 'data of second future')`
///
/// In general, the latter will be produced only when the new future is
/// non-null, and the former only when the old future is non-null.
Expand Down Expand Up @@ -768,10 +792,10 @@ class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
_snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
});
}
}, onError: (Object error) {
}, onError: (Object error, StackTrace stackTrace) {
if (_activeCallbackIdentity == callbackIdentity) {
setState(() {
_snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
_snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error, stackTrace);
});
}
});
Expand Down
Loading