Commit c3fab029 authored by Brian Egan's avatar Brian Egan Committed by Mikkel Nygaard Ravn

Idea: Provide initial data to the StreamBuilder (#13820)

parent c669c62e
......@@ -267,13 +267,13 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
/// a [Stream].
///
/// Widget rebuilding is scheduled by each interaction, using [State.setState],
/// but is otherwise decoupled from the timing of the stream. The [build] method
/// but is otherwise decoupled from the timing of the stream. The [builder]
/// is called at the discretion of the Flutter pipeline, and will thus receive a
/// timing-dependent sub-sequence of the snapshots that represent the
/// interaction with the stream.
///
/// As an example, when interacting with a stream producing the integers
/// 0 through 9, the [build] method may be called with any ordered sub-sequence
/// 0 through 9, the [builder] may be called with any ordered sub-sequence
/// of the following snapshots that includes the last one (the one with
/// ConnectionState.done):
///
......@@ -284,8 +284,9 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 9)`
/// * `new AsyncSnapshot<int>.withData(ConnectionState.done, 9)`
///
/// The actual sequence of invocations of [build] depends on the relative timing
/// of events produced by the stream and the build rate of the Flutter pipeline.
/// 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
/// pipeline.
///
/// Changing the [StreamBuilder] configuration to another stream during event
/// generation introduces snapshot pairs of the form
......@@ -303,6 +304,11 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
/// The data and error fields of snapshots produced are only changed when the
/// state is `ConnectionState.active`.
///
/// 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 first event arrives on the stream, the snapshot carries data of
/// your choice rather than the default null value.
///
/// See also:
///
/// * [StreamBuilderBase], which supports widget building based on a computation
......@@ -333,19 +339,25 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
/// 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].
/// strategy is given by [builder]. The [initialData] is used to create the
/// initial snapshot. It is null by default.
const StreamBuilder({
Key key,
this.initialData,
Stream<T> stream,
@required this.builder
}) : assert(builder != null),
})
: assert(builder != null),
super(key: key, stream: stream);
/// The build strategy currently used by this builder. Cannot be null.
final AsyncWidgetBuilder<T> builder;
/// The data that will be used to create the initial snapshot. Null by default.
final T initialData;
@override
AsyncSnapshot<T> initial() => new AsyncSnapshot<T>.nothing(); // ignore: prefer_const_constructors
AsyncSnapshot<T> initial() => new AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
@override
AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
......@@ -391,6 +403,11 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
/// * `new AsyncSnapshot<String>.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.
///
/// 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
/// when changing the [FutureBuilder] configuration to another future. If the
......@@ -437,6 +454,7 @@ class FutureBuilder<T> extends StatefulWidget {
const FutureBuilder({
Key key,
this.future,
this.initialData,
@required this.builder
}) : assert(builder != null),
super(key: key);
......@@ -448,6 +466,9 @@ class FutureBuilder<T> extends StatefulWidget {
/// The build strategy currently used by this builder. Cannot be null.
final AsyncWidgetBuilder<T> builder;
/// The data that will be used to create the initial snapshot. Null by default.
final T initialData;
@override
State<FutureBuilder<T>> createState() => new _FutureBuilderState<T>();
}
......@@ -458,11 +479,12 @@ class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
/// calling setState from stale callbacks, e.g. after disposal of this state,
/// or after widget reconfiguration to a new Future.
Object _activeCallbackIdentity;
AsyncSnapshot<T> _snapshot = new AsyncSnapshot<T>.nothing(); // ignore: prefer_const_constructors
AsyncSnapshot<T> _snapshot;
@override
void initState() {
super.initState();
_snapshot = new AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
_subscribe();
}
......
......@@ -118,6 +118,34 @@ void main() {
await eventFiring(tester);
expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget);
});
testWidgets('runs the builder using given initial data', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
await tester.pumpWidget(new FutureBuilder<String>(
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 = new GlobalKey();
await tester.pumpWidget(new FutureBuilder<String>(
key: key,
future: null,
builder: snapshotText,
initialData: 'I',
));
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget);
final Completer<String> completer = new Completer<String>();
await tester.pumpWidget(new FutureBuilder<String>(
key: key,
future: completer.future,
builder: snapshotText,
initialData: 'Ignored',
));
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
});
});
group('StreamBuilder', () {
testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async {
......@@ -180,6 +208,33 @@ void main() {
await eventFiring(tester);
expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget);
});
testWidgets('runs the builder using given initial data', (WidgetTester tester) async {
final StreamController<String> controller = new StreamController<String>();
await tester.pumpWidget(new StreamBuilder<String>(
stream: controller.stream,
builder: snapshotText,
initialData: 'I',
));
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
});
testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
await tester.pumpWidget(new StreamBuilder<String>(
key: key,
stream: null,
builder: snapshotText,
initialData: 'I',
));
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget);
final StreamController<String> controller = new StreamController<String>();
await tester.pumpWidget(new StreamBuilder<String>(
key: key,
stream: controller.stream,
builder: snapshotText,
initialData: 'Ignored',
));
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
});
});
group('FutureBuilder and StreamBuilder behave identically on Stream from Future', () {
testWidgets('when completing with data', (WidgetTester tester) async {
......@@ -211,6 +266,24 @@ void main() {
]));
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(new Column(children: <Widget>[
new FutureBuilder<String>(future: null, builder: snapshotText, initialData: 'I'),
new StreamBuilder<String>(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<String> completer = new Completer<String>();
await tester.pumpWidget(new Column(children: <Widget>[
new FutureBuilder<String>(future: completer.future, builder: snapshotText, initialData: 'I'),
new StreamBuilder<String>(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));
});
});
group('StreamBuilderBase', () {
testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async {
......
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