diff --git a/packages/flutter/lib/src/widgets/async.dart b/packages/flutter/lib/src/widgets/async.dart index 7491908925..18844f59e6 100644 --- a/packages/flutter/lib/src/widgets/async.dart +++ b/packages/flutter/lib/src/widgets/async.dart @@ -59,35 +59,41 @@ abstract class StreamBuilderBase extends StatefulWidget { /// /// Sub-classes must override this method to provide the initial value for /// the fold computation. + @protected S initial(); /// Returns an updated version of the [current] summary reflecting that we /// are now connected to a stream. /// /// The default implementation returns [current] as is. + @protected S afterConnected(S current) => current; /// Returns an updated version of the [current] summary following a data event. /// /// Sub-classes must override this method to specify how the current summary /// is combined with the new data item in the fold computation. + @protected S afterData(S current, T data); /// Returns an updated version of the [current] summary following an error. /// /// The default implementation returns [current] as is. + @protected S afterError(S current, Object error) => current; /// Returns an updated version of the [current] summary following stream /// termination. /// /// The default implementation returns [current] as is. + @protected S afterDone(S current) => current; /// Returns an updated version of the [current] summary reflecting that we /// are no longer connected to a stream. /// /// The default implementation returns [current] as is. + @protected S afterDisconnected(S current) => current; /// Returns a Widget based on the [currentSummary]. @@ -185,6 +191,8 @@ enum ConnectionState { /// Immutable representation of the most recent interaction with an asynchronous /// computation. /// +/// `T` is the type of computation data. +/// /// See also: /// /// * [StreamBuilder], which builds itself based on a snapshot from interacting @@ -193,46 +201,68 @@ enum ConnectionState { /// with a [Future]. @immutable class AsyncSnapshot { - /// Creates an [AsyncSnapshot] with the specified [connectionState], - /// and optionally either [data] or [error] (but not both). - const AsyncSnapshot._(this.connectionState, this.data, this.error) + /// Creates an [AsyncSnapshot] with the specified [connectionState] and + /// [hasData], and optionally either [data] or [error] (but not both). + /// + /// It is legal for both [hasData] to be true and [data] to be null. + const AsyncSnapshot._(this.connectionState, this.hasData, this._data, this.error) : assert(connectionState != null), - assert(!(data != null && error != null)); + assert(hasData != null), + assert(hasData || _data == null), + assert(!(hasData && error != null)); - /// Creates an [AsyncSnapshot] in [ConnectionState.none] with null data and error. - const AsyncSnapshot.nothing() : this._(ConnectionState.none, null, null); + /// Creates an [AsyncSnapshot] in the specified [state] and with neither + /// [data] nor [error]. + const AsyncSnapshot.withoutData(ConnectionState state) : this._(state, false, 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); + /// Creates an [AsyncSnapshot] in the specified [state] and with the + /// specified [data] (possibly null). + const AsyncSnapshot.withData(ConnectionState state, T data) : this._(state, true, data, 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` and with the + /// specified [error]. + const AsyncSnapshot.withError(ConnectionState state, Object error) : this._(state, false, null, error); - /// Current state of connection to the asynchronous computation. + /// The current state of the connection to the asynchronous computation. + /// + /// This property exists independently of the [data] and [error] properties. + /// In other words, a snapshot can exist with any combination of + /// (`connectionState`/`data`) or (`connectionState`/`error`) tuples. + /// + /// This is guaranteed to be non-null. final ConnectionState connectionState; - /// The latest data received by the asynchronous computation. + /// Whether this snapshot contains [data]. /// - /// If this is non-null, [hasData] will be true. + /// This can be false even when the asynchronous computation has completed + /// successfully ([connectionState] is [ConnectionState.done]), if the + /// computation did not return a value. For example, a [Future] will + /// complete with no data even if it completes successfully. /// - /// If [error] is not null, this will be null. See [hasError]. - /// - /// If the asynchronous computation has never returned a value, this may be - /// set to an initial data value specified by the relevant widget. See - /// [FutureBuilder.initialData] and [StreamBuilder.initialData]. - final T data; + /// If this property is false, then attempting to access the [data] property + /// will throw an exception. + final bool hasData; - /// Returns latest data received, failing if there is no data. + /// The latest data received by the asynchronous computation, failing if + /// there is no data. /// - /// Throws [error], if [hasError]. Throws [StateError], if neither [hasData] - /// nor [hasError]. - T get requireData { + /// If [hasData] is true, accessing this property will not throw an error. + /// + /// If [error] is not null, attempting to access this property will throw + /// [error]. See [hasError]. + /// + /// If neither [hasData] nor [hasError] is true, then accessing this + /// property will throw a [StateError]. + T get data { if (hasData) - return data; - if (hasError) + return _data; + if (hasError) { + // TODO(tvolkert): preserve the stack trace (https://github.com/dart-lang/sdk/issues/30741) throw error; + } throw StateError('Snapshot has neither data nor error'); } + final T _data; /// The latest error object received by the asynchronous computation. /// @@ -243,41 +273,46 @@ class AsyncSnapshot { /// 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 inState(ConnectionState state) => AsyncSnapshot._(state, data, error); - - /// Returns whether this snapshot contains a non-null [data] value. - /// - /// This can be false even when the asynchronous computation has completed - /// successfully, if the computation did not return a non-null value. For - /// example, a [Future] will complete with the null value even if it - /// completes successfully. - bool get hasData => data != null; + /// The [hasData], [data], [hasError], and [error] fields persist unmodified, + /// even if the new state is [ConnectionState.none]. + AsyncSnapshot inState(ConnectionState state) => AsyncSnapshot._(state, hasData, _data, error); /// Returns whether this snapshot contains a non-null [error] value. /// /// This is always true if the asynchronous computation's last result was /// failure. + /// + /// When this is true, [hasData] will always be false. bool get hasError => error != null; @override - String toString() => '$runtimeType($connectionState, $data, $error)'; + String toString() { + final StringBuffer buffer = StringBuffer()..write('$runtimeType') + ..write('(') + ..write('$connectionState'); + if (hasData) + buffer.write(', data: $_data'); + else if (hasError) + buffer.write(', error: $error'); + buffer.write(')'); + return buffer.toString(); + } @override bool operator ==(dynamic other) { if (identical(this, other)) return true; - if (other is! AsyncSnapshot) + if (runtimeType != other.runtimeType) return false; final AsyncSnapshot typedOther = other; return connectionState == typedOther.connectionState - && data == typedOther.data + && hasData == typedOther.hasData + && _data == typedOther._data && error == typedOther.error; } @override - int get hashCode => hashValues(connectionState, data, error); + int get hashCode => hashValues(connectionState, hasData, _data, error); } /// Signature for strategies that build widgets based on asynchronous @@ -307,12 +342,12 @@ typedef AsyncWidgetBuilder = Widget Function(BuildContext context, AsyncSnaps /// of the following snapshots that includes the last one (the one with /// ConnectionState.done): /// -/// * `new AsyncSnapshot.withData(ConnectionState.waiting, null)` -/// * `new AsyncSnapshot.withData(ConnectionState.active, 0)` -/// * `new AsyncSnapshot.withData(ConnectionState.active, 1)` +/// * `AsyncSnapshot.withData(ConnectionState.waiting, null)` +/// * `AsyncSnapshot.withData(ConnectionState.active, 0)` +/// * `AsyncSnapshot.withData(ConnectionState.active, 1)` /// * ... -/// * `new AsyncSnapshot.withData(ConnectionState.active, 9)` -/// * `new AsyncSnapshot.withData(ConnectionState.done, 9)` +/// * `AsyncSnapshot.withData(ConnectionState.active, 9)` +/// * `AsyncSnapshot.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 @@ -329,7 +364,7 @@ typedef AsyncWidgetBuilder = Widget Function(BuildContext context, AsyncSnaps /// /// The stream may produce errors, resulting in snapshots of the form: /// -/// * `new AsyncSnapshot.withError(ConnectionState.active, 'some error')` +/// * `AsyncSnapshot.withError(ConnectionState.active, 'some error')` /// /// The data and error fields of snapshots produced are only changed when the /// state is `ConnectionState.active`. @@ -337,7 +372,20 @@ typedef AsyncWidgetBuilder = Widget Function(BuildContext context, AsyncSnaps /// The initial snapshot data can be controlled by specifying [initialData]. /// This should be used to ensure that the first frame has the expected value, /// as the builder will always be called before the stream listener has a chance -/// to be processed. +/// to be processed. In cases where callers wish to have no initial data, the +/// [new StreamBuilder.withoutInitialData] constructor may be used. Doing so +/// may cause the first frame to have a snapshot that contains no data. +/// +/// ## Void StreamBuilders +/// +/// The `StreamBuilder` type will produce snapshots that contain no data. +/// An example stream of snapshots would be the following: +/// +/// * `AsyncSnapshot.withoutData(ConnectionState.waiting)` +/// * `AsyncSnapshot.withoutData(ConnectionState.active)` +/// * ... +/// * `AsyncSnapshot.withoutData(ConnectionState.active)` +/// * `AsyncSnapshot.withoutData(ConnectionState.done)` /// /// {@tool sample} /// @@ -348,6 +396,7 @@ typedef AsyncWidgetBuilder = Widget Function(BuildContext context, AsyncSnaps /// ```dart /// StreamBuilder( /// stream: _lot?.bids, // a Stream or null +/// initialData: 100, // initial seed value /// builder: (BuildContext context, AsyncSnapshot snapshot) { /// if (snapshot.hasError) /// return Text('Error: ${snapshot.error}'); @@ -372,42 +421,95 @@ typedef AsyncWidgetBuilder = Widget Function(BuildContext context, AsyncSnaps // TODO(ianh): remove unreachable code above once https://github.com/dart-lang/linter/issues/1139 is fixed class StreamBuilder extends StreamBuilderBase> { /// Creates a new [StreamBuilder] that builds itself based on the latest - /// snapshot of interaction with the specified [stream] and whose build + /// snapshot of interaction with the specified `stream` and whose build /// strategy is given by [builder]. /// - /// The [initialData] is used to create the initial snapshot. + /// The [initialData] argument is used to create the initial snapshot. For + /// cases where there is no initial snapshot or the initial snapshot is not + /// yet available, callers may construct a [StreamBuilder] without an initial + /// snapshot using [new StreamBuilder.withoutInitialData]. /// /// The [builder] must not be null. const StreamBuilder({ Key key, - this.initialData, + @required T initialData, Stream stream, @required this.builder, }) : assert(builder != null), + hasInitialData = true, + _initialData = initialData, + super(key: key, stream: stream); + + /// Creates a new [StreamBuilder] that builds itself based on the latest + /// snapshot of interaction with the specified `stream` and whose build + /// strategy is given by [builder]. + /// + /// The initial snapshot will contain no data. + /// + /// The [builder] must not be null. + const StreamBuilder.withoutInitialData({ + Key key, + Stream stream, + @required this.builder, + }) : assert(builder != null), + hasInitialData = false, + _initialData = null, super(key: key, stream: stream); /// The build strategy currently used by this builder. final AsyncWidgetBuilder builder; + /// Whether this builder's initial snapshot contains data. + /// + /// If this is false, then attempting to access [initialData] will throw an + /// error. + /// + /// See also: + /// + /// * [AsyncSnapshot.hasData], the corresponding property that will be set + /// in the initial snapshot. + final bool hasInitialData; + /// The data that will be used to create the initial snapshot. /// /// Providing this value (presumably obtained synchronously somehow when the /// [Stream] was created) ensures that the first frame will show useful data. - /// Otherwise, the first frame will be built with the value null, regardless - /// of whether a value is available on the stream: since streams are - /// asynchronous, no events from the stream can be obtained before the initial - /// build. - final T initialData; + /// Otherwise, the first frame will be built with a snapshot that contains no + /// data, regardless of whether a value is available on the stream: since + /// streams are asynchronous, no events from the stream can be obtained + /// before the initial build. + /// + /// Some builders intentionally have no data when first built. For those + /// cases, callers can use the [new StreamBuilder.withoutInitialData] + /// constructor. When a builder was constructed in this way, attempting to + /// access the [initialData] property will throw a [StateError]. + T get initialData { + if (!hasInitialData) { + throw StateError( + 'StreamBuilder was created without initial data, yet the initialData ' + 'property was accessed. If you wish your StreamBuilder to have initial ' + 'data, create it using the default constructor.', + ); + } + return _initialData; + } + final T _initialData; @override - AsyncSnapshot initial() => AsyncSnapshot.withData(ConnectionState.none, initialData); + AsyncSnapshot initial() { + return hasInitialData + ? AsyncSnapshot.withData(ConnectionState.none, initialData) + : AsyncSnapshot.withoutData(ConnectionState.none); + } @override AsyncSnapshot afterConnected(AsyncSnapshot current) => current.inState(ConnectionState.waiting); @override AsyncSnapshot afterData(AsyncSnapshot current, T data) { - return AsyncSnapshot.withData(ConnectionState.active, data); + return _TypeLiteral.isVoidType(T) + ? AsyncSnapshot.withoutData(ConnectionState.active) + : AsyncSnapshot.withData(ConnectionState.active, data); } @override @@ -455,23 +557,17 @@ class StreamBuilder extends StreamBuilderBase> { /// /// ## Builder contract /// -/// For a future that completes successfully with data, assuming [initialData] -/// is null, the [builder] will be called with either both or only the latter of -/// the following snapshots: +/// For a future that completes successfully with data, the [builder] will be +/// called with either both or only the latter of the following snapshots: /// -/// * `new AsyncSnapshot.withData(ConnectionState.waiting, null)` -/// * `new AsyncSnapshot.withData(ConnectionState.done, 'some data')` +/// * `AsyncSnapshot.withoutData(ConnectionState.waiting)` +/// * `AsyncSnapshot.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.withData(ConnectionState.waiting, null)` -/// * `new AsyncSnapshot.withError(ConnectionState.done, 'some error')` -/// -/// The initial snapshot data can be controlled by specifying [initialData]. You -/// would use this facility to ensure that if the [builder] is invoked before -/// the future completes, the snapshot carries data of your choice rather than -/// the default null value. +/// * `AsyncSnapshot.withoutData(ConnectionState.waiting)` +/// * `AsyncSnapshot.withError(ConnectionState.done, 'some error')` /// /// The data and error fields of the snapshot change only as the connection /// state field transitions from `waiting` to `done`, and they will be retained @@ -479,8 +575,8 @@ class StreamBuilder extends StreamBuilderBase> { /// 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.withData(ConnectionState.none, 'data of first future')` -/// * `new AsyncSnapshot.withData(ConnectionState.waiting, 'data of second future')` +/// * `AsyncSnapshot.withData(ConnectionState.none, 'data of first future')` +/// * `AsyncSnapshot.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. @@ -489,6 +585,12 @@ class StreamBuilder extends StreamBuilderBase> { /// `future?.asStream()`, except that snapshots with `ConnectionState.active` /// may appear for the latter, depending on how the stream is implemented. /// +/// ## Void futures +/// +/// The `FutureBuilder` type will produce snapshots that contain no data: +/// +/// * `AsyncSnapshot.withoutData(ConnectionState.done)` +/// /// {@tool sample} /// /// This sample shows a [FutureBuilder] configuring a text label to show the @@ -497,7 +599,13 @@ class StreamBuilder extends StreamBuilderBase> { /// /// ```dart /// FutureBuilder( -/// future: _calculation, // a previously-obtained Future or null +/// // A previously-obtained `Future` or null. +/// // +/// // This MUST NOT be created during the call to the `build()` method that +/// // creates the `FutureBuilder`. Doing so will cause a new future to be +/// // instantiated every time `build()` is called (potentially every frame). +/// future: _calculation, +/// /// builder: (BuildContext context, AsyncSnapshot snapshot) { /// switch (snapshot.connectionState) { /// case ConnectionState.none: @@ -520,11 +628,33 @@ class FutureBuilder extends StatefulWidget { /// Creates a widget that builds itself based on the latest snapshot of /// interaction with a [Future]. /// - /// The [builder] must not be null. + /// The [future] argument must have been obtained earlier, e.g. during + /// [State.initState], [State.didUpdateConfig], or + /// [State.didChangeDependencies]. It must not be created during the + /// [State.build] or [StatelessWidget.build] method call when constructing + /// the [FutureBuilder]. If the [future] is created at the same time as the + /// [FutureBuilder], then every time the [FutureBuilder]'s parent is rebuilt, + /// the asynchronous task will be restarted. + /// + // ignore: deprecated_member_use_from_same_package + /// The [initialData] argument specifies the data that will be used to create + /// the snapshots provided to [builder] until a non-null [future] has + /// completed. This argument is deprecated and will be removed in a future + /// stable release because snapshots that are provided to the [builder] + /// contain an [AsyncSnapshot.connectionState] property that indicates the + /// state of the [future]. The builder can use that connection state to + /// provide an "initial value" when the future has not yet completed. + /// + /// The [builder] argument must not be null. const FutureBuilder({ Key key, this.future, - this.initialData, + @Deprecated( + 'Instead of providing initialData to FutureBuilder, consider checking ' + 'for ConnectionState.none or ConnectionState.waiting in your build() ' + 'method to know whether the future has completed or not.', + ) + this.initialData, // ignore: deprecated_member_use_from_same_package @required this.builder, }) : assert(builder != null), super(key: key); @@ -533,7 +663,10 @@ class FutureBuilder extends StatefulWidget { /// possibly null. /// /// If no future has yet completed, including in the case where [future] is - /// null, the data provided to the [builder] will be set to [initialData]. + // ignore: deprecated_member_use_from_same_package + /// null, the snapshot provided to the [builder] will contain [initialData] + /// if this widget was created with initial data or will contain no data if + /// this widget was created without initial data. final Future future; /// The build strategy currently used by this builder. @@ -542,29 +675,56 @@ class FutureBuilder extends StatefulWidget { /// [AsyncSnapshot.connectionState] property will be one of the following /// values: /// - /// * [ConnectionState.none]: [future] is null. The [AsyncSnapshot.data] will - /// be set to [initialData], unless a future has previously completed, in - /// which case the previous result persists. + /// * [ConnectionState.none]: [future] is null. /// - /// * [ConnectionState.waiting]: [future] is not null, but has not yet - /// completed. The [AsyncSnapshot.data] will be set to [initialData], - /// unless a future has previously completed, in which case the previous - /// result persists. + /// If this widget was created with initial data (deprecated), then the + /// [AsyncSnapshot.data] will be set to [initialData], unless a future has + /// previously completed, in which case the previous result persists. + /// + /// If this widget was created without initial data, then the + /// [AsyncSnapshot.data] will be unset, and attempts to access the data + /// will result in an exception. + /// + /// * [ConnectionState.waiting]: [future] is not null but has not yet + /// completed. + /// + /// If this widget was created with initial data (deprecated), then the + /// [AsyncSnapshot.data] will be set to [initialData], unless a future has + /// previously completed, in which case the previous result persists. + /// + /// If this widget was created without initial data, then the + /// [AsyncSnapshot.data] will be unset, and attempts to access the data + /// will result in an exception. /// /// * [ConnectionState.done]: [future] is not null, and has completed. If the /// future completed successfully, the [AsyncSnapshot.data] will be set to /// the value to which the future completed. If it completed with an error, /// [AsyncSnapshot.hasError] will be true and [AsyncSnapshot.error] will be /// set to the error object. + /// + /// In the case of [future] being a [Future], the snapshot will not + /// contain data even if the future completed successfully. final AsyncWidgetBuilder builder; /// The data that will be used to create the snapshots provided until a /// non-null [future] has completed. /// - /// If the future completes with an error, the data in the [AsyncSnapshot] - /// provided to the [builder] will become null, regardless of [initialData]. - /// (The error itself will be available in [AsyncSnapshot.error], and + /// If the future completes with an error, the [AsyncSnapshot] provided to + /// the [builder] will contain no data, regardless of [initialData]. (The + /// error itself will be available in [AsyncSnapshot.error], and /// [AsyncSnapshot.hasError] will be true.) + /// + /// This field is deprecated and will be removed in a future stable release + /// because snapshots that are provided to the [builder] contain an + /// [AsyncSnapshot.connectionState] property that indicates the state of the + /// [future]. The builder can use that connection state to provide an + /// "initial value" when the future has not yet completed. + @Deprecated( + 'Instead of using FutureBuilder.initialData, consider checking ' + 'for ConnectionState.none or ConnectionState.waiting in your build() ' + 'ConnectionState.none or ConnectionState.waiting in your build() ' + 'method to know whether the future has completed or not.', + ) final T initialData; @override @@ -582,7 +742,11 @@ class _FutureBuilderState extends State> { @override void initState() { super.initState(); - _snapshot = AsyncSnapshot.withData(ConnectionState.none, widget.initialData); + // ignore: deprecated_member_use_from_same_package + _snapshot = widget.initialData == null + ? AsyncSnapshot.withoutData(ConnectionState.none) + // ignore: deprecated_member_use_from_same_package + : AsyncSnapshot.withData(ConnectionState.none, widget.initialData); _subscribe(); } @@ -614,7 +778,9 @@ class _FutureBuilderState extends State> { widget.future.then((T data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + _snapshot = _TypeLiteral.isVoidType(T) + ? AsyncSnapshot.withoutData(ConnectionState.done) + : AsyncSnapshot.withData(ConnectionState.done, data); }); } }, onError: (Object error) { @@ -632,3 +798,33 @@ class _FutureBuilderState extends State> { _activeCallbackIdentity = null; } } + +/// Class that allows callers to reference instances of [Type] that would +/// otherwise not be valid expressions. +/// +/// Generic types and the `void` type are not usable as Dart expressions, so +/// the following statements are not legal and all yield compile-time errors: +/// +/// ```dart +/// if (type == List) print('msg'); +/// if (type == void) print('msg'); +/// Type type = List; +/// ``` +/// +/// This class allows callers to get handles on such types, like so: +/// +/// ```dart +/// if (type == const _TypeLiteral>().type) print('msg'); +/// if (type == const _TypeLiteral().type) print('msg'); +/// Type type = const _TypeLiteral>().type; +/// ``` +class _TypeLiteral { + /// Creates a new [_TypeLiteral]. + const _TypeLiteral(); + + /// Returns whether the specified type represents a "void" type. + static bool isVoidType(Type type) => type == const _TypeLiteral().type; + + /// The [Type] (`T`) represented by this [_TypeLiteral]. + Type get type => T; +} diff --git a/packages/flutter/test/widgets/async_test.dart b/packages/flutter/test/widgets/async_test.dart index d8af97a5da..6e559af782 100644 --- a/packages/flutter/test/widgets/async_test.dart +++ b/packages/flutter/test/widgets/async_test.dart @@ -12,23 +12,33 @@ void main() { return Text(snapshot.toString(), textDirection: TextDirection.ltr); } group('AsyncSnapshot', () { - test('requiring data succeeds if data is present', () { - expect( - const AsyncSnapshot.withData(ConnectionState.done, 'hello').requireData, - 'hello', - ); + test('data succeeds if data is present', () { + const AsyncSnapshot snapshot = AsyncSnapshot.withData(ConnectionState.done, 'hello'); + expect(snapshot.hasData, isTrue); + expect(snapshot.data, 'hello'); + expect(snapshot.hasError, isFalse); + expect(snapshot.error, isNull); }); - test('requiring data fails if there is an error', () { - expect( - () => const AsyncSnapshot.withError(ConnectionState.done, 'error').requireData, - throwsA(equals('error')), - ); + test('data throws if there is an error', () { + const AsyncSnapshot snapshot = AsyncSnapshot.withError(ConnectionState.done, 'error'); + expect(snapshot.hasData, isFalse); + expect(() => snapshot.data, throwsA(equals('error'))); + expect(snapshot.hasError, isTrue); + expect(snapshot.error, 'error'); }); - test('requiring data fails if snapshot has neither data nor error', () { - expect( - () => const AsyncSnapshot.nothing().requireData, - throwsStateError, - ); + test('data throws if created without data', () { + const AsyncSnapshot snapshot = AsyncSnapshot.withoutData(ConnectionState.none); + expect(snapshot.hasData, isFalse); + expect(() => snapshot.data, throwsStateError); + expect(snapshot.hasError, isFalse); + expect(snapshot.error, isNull); + }); + test('data can be null', () { + const AsyncSnapshot snapshot = AsyncSnapshot.withData(ConnectionState.none, null); + expect(snapshot.hasData, isTrue); + expect(snapshot.data, isNull); + expect(snapshot.hasError, isFalse); + expect(snapshot.error, isNull); }); }); group('Async smoke tests', () { @@ -40,7 +50,7 @@ void main() { await eventFiring(tester); }); testWidgets('StreamBuilder', (WidgetTester tester) async { - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( stream: Stream.fromIterable(['hello', 'world']), builder: snapshotText, )); @@ -59,12 +69,12 @@ void main() { await tester.pumpWidget(FutureBuilder( key: key, future: null, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsOneWidget); final Completer completer = Completer(); await tester.pumpWidget(FutureBuilder( key: key, future: completer.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); }); testWidgets('gracefully handles transition to null future', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -72,14 +82,14 @@ void main() { await tester.pumpWidget(FutureBuilder( key: key, future: completer.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); await tester.pumpWidget(FutureBuilder( key: key, future: null, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsOneWidget); completer.complete('hello'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsOneWidget); }); testWidgets('gracefully handles transition to other future', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -88,125 +98,132 @@ void main() { await tester.pumpWidget(FutureBuilder( key: key, future: completerA.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); await tester.pumpWidget(FutureBuilder( key: key, future: completerB.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); completerB.complete('B'); completerA.complete('A'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: B)'), findsOneWidget); }); testWidgets('tracks life-cycle of Future to success', (WidgetTester tester) async { final Completer completer = Completer(); await tester.pumpWidget(FutureBuilder( future: completer.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); completer.complete('hello'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: hello)'), findsOneWidget); }); testWidgets('tracks life-cycle of Future to error', (WidgetTester tester) async { final Completer completer = Completer(); await tester.pumpWidget(FutureBuilder( future: completer.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); completer.completeError('bad'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.done, error: bad)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(FutureBuilder( - key: key, - future: null, - builder: snapshotText, - initialData: 'I', - )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); - }); - testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(FutureBuilder( - key: key, - future: null, - builder: snapshotText, - initialData: 'I', - )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); + testWidgets('produces snapshot with null data for null-completing data Future', (WidgetTester tester) async { final Completer completer = Completer(); await tester.pumpWidget(FutureBuilder( - key: key, - future: completer.future, - builder: snapshotText, - initialData: 'Ignored', + future: completer.future, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + completer.complete(null); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: null)'), findsOneWidget); + }); + testWidgets('produces snapshot with null data for Future', (WidgetTester tester) async { + final Completer completer = Completer(); // ignore: prefer_void_to_null + await tester.pumpWidget(FutureBuilder( // ignore: prefer_void_to_null + future: completer.future, builder: snapshotText, + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + completer.complete(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: null)'), findsOneWidget); + }); + testWidgets('produces snapshot with no data for Future', (WidgetTester tester) async { + final Completer completer = Completer(); + await tester.pumpWidget( + FutureBuilder( + future: completer.future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Text(snapshot.toString(), textDirection: TextDirection.ltr); + }, + ), + ); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + completer.complete(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done)'), findsOneWidget); }); }); group('StreamBuilder', () { testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: null, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsOneWidget); final StreamController controller = StreamController(); - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: controller.stream, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); }); testWidgets('gracefully handles transition to null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController controller = StreamController(); - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: controller.stream, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); - await tester.pumpWidget(StreamBuilder( + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: null, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsOneWidget); }); testWidgets('gracefully handles transition to other stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController controllerA = StreamController(); final StreamController controllerB = StreamController(); - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: controllerA.stream, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); - await tester.pumpWidget(StreamBuilder( + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: controllerB.stream, builder: snapshotText, )); controllerB.add('B'); controllerA.add('A'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.active, B, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.active, data: B)'), findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController controller = StreamController(); - await tester.pumpWidget(StreamBuilder( + await tester.pumpWidget(StreamBuilder.withoutInitialData( key: key, stream: controller.stream, builder: snapshotText, )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); controller.add('1'); controller.add('2'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.active, 2, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.active, data: 2)'), findsOneWidget); controller.add('3'); controller.addError('bad'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.active, null, bad)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.active, error: bad)'), findsOneWidget); controller.add('4'); controller.close(); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: 4)'), findsOneWidget); }); testWidgets('runs the builder using given initial data', (WidgetTester tester) async { final StreamController controller = StreamController(); @@ -215,7 +232,7 @@ void main() { builder: snapshotText, initialData: 'I', )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting, data: I)'), findsOneWidget); }); testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -225,7 +242,7 @@ void main() { builder: snapshotText, initialData: 'I', )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.none, data: I)'), findsOneWidget); final StreamController controller = StreamController(); await tester.pumpWidget(StreamBuilder( key: key, @@ -233,7 +250,69 @@ void main() { builder: snapshotText, initialData: 'Ignored', )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); + expect(find.text('AsyncSnapshot(ConnectionState.waiting, data: I)'), findsOneWidget); + }); + testWidgets('produces snapshots with null data for null-producing stream', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + final StreamController controller = StreamController(); + await tester.pumpWidget(StreamBuilder.withoutInitialData( + key: key, + stream: controller.stream, + builder: snapshotText, + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + controller.add(null); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, data: null)'), findsOneWidget); + controller.addError('bad'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, error: bad)'), findsOneWidget); + controller.add(null); + controller.close(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: null)'), findsOneWidget); + }); + testWidgets('produces snapshots with null data for Stream', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + final StreamController controller = StreamController(); // ignore: prefer_void_to_null + await tester.pumpWidget(StreamBuilder.withoutInitialData( // ignore: prefer_void_to_null + key: key, + stream: controller.stream, + builder: snapshotText, + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + controller.add(null); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, data: null)'), findsOneWidget); + controller.addError('bad'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, error: bad)'), findsOneWidget); + controller.add(null); + controller.close(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: null)'), findsOneWidget); + }); + testWidgets('produces snapshots with no data for Stream', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + final StreamController controller = StreamController(); + await tester.pumpWidget(StreamBuilder.withoutInitialData( + key: key, + stream: controller.stream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Text(snapshot.toString(), textDirection: TextDirection.ltr); + }, + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsOneWidget); + controller.add(null); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active)'), findsOneWidget); + controller.addError('bad'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, error: bad)'), findsOneWidget); + controller.add(null); + controller.close(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done)'), findsOneWidget); }); }); group('FutureBuilder and StreamBuilder behave identically on Stream from Future', () { @@ -241,48 +320,30 @@ void main() { final Completer completer = Completer(); await tester.pumpWidget(Column(children: [ FutureBuilder(future: completer.future, builder: snapshotText), - StreamBuilder(stream: completer.future.asStream(), builder: snapshotText), + StreamBuilder.withoutInitialData(stream: completer.future.asStream(), builder: snapshotText), ])); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsNWidgets(2)); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsNWidgets(2)); completer.complete('hello'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsNWidgets(2)); + expect(find.text('AsyncSnapshot(ConnectionState.done, data: hello)'), findsNWidgets(2)); }); testWidgets('when completing with error', (WidgetTester tester) async { final Completer completer = Completer(); await tester.pumpWidget(Column(children: [ FutureBuilder(future: completer.future, builder: snapshotText), - StreamBuilder(stream: completer.future.asStream(), builder: snapshotText), + StreamBuilder.withoutInitialData(stream: completer.future.asStream(), builder: snapshotText), ])); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsNWidgets(2)); + expect(find.text('AsyncSnapshot(ConnectionState.waiting)'), findsNWidgets(2)); completer.completeError('bad'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsNWidgets(2)); + expect(find.text('AsyncSnapshot(ConnectionState.done, error: bad)'), findsNWidgets(2)); }); testWidgets('when Future is null', (WidgetTester tester) async { await tester.pumpWidget(Column(children: [ FutureBuilder(future: null, builder: snapshotText), - StreamBuilder(stream: null, builder: snapshotText), + StreamBuilder.withoutInitialData(stream: null, builder: snapshotText), ])); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsNWidgets(2)); - }); - testWidgets('when initialData is used with null Future and Stream', (WidgetTester tester) async { - await tester.pumpWidget(Column(children: [ - FutureBuilder(future: null, builder: snapshotText, initialData: 'I'), - StreamBuilder(stream: null, builder: snapshotText, initialData: 'I'), - ])); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsNWidgets(2)); - }); - testWidgets('when using initialData and completing with data', (WidgetTester tester) async { - final Completer completer = Completer(); - await tester.pumpWidget(Column(children: [ - FutureBuilder(future: completer.future, builder: snapshotText, initialData: 'I'), - StreamBuilder(stream: completer.future.asStream(), builder: snapshotText, initialData: 'I'), - ])); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsNWidgets(2)); - completer.complete('hello'); - await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsNWidgets(2)); + expect(find.text('AsyncSnapshot(ConnectionState.none)'), findsNWidgets(2)); }); }); group('StreamBuilderBase', () {