Skip to content

Commit c3fab02

Browse files
brianeganmravn-google
authored andcommitted
Idea: Provide initial data to the StreamBuilder (flutter#13820)
1 parent c669c62 commit c3fab02

File tree

2 files changed

+104
-9
lines changed

2 files changed

+104
-9
lines changed

packages/flutter/lib/src/widgets/async.dart

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,13 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
267267
/// a [Stream].
268268
///
269269
/// Widget rebuilding is scheduled by each interaction, using [State.setState],
270-
/// but is otherwise decoupled from the timing of the stream. The [build] method
270+
/// but is otherwise decoupled from the timing of the stream. The [builder]
271271
/// is called at the discretion of the Flutter pipeline, and will thus receive a
272272
/// timing-dependent sub-sequence of the snapshots that represent the
273273
/// interaction with the stream.
274274
///
275275
/// As an example, when interacting with a stream producing the integers
276-
/// 0 through 9, the [build] method may be called with any ordered sub-sequence
276+
/// 0 through 9, the [builder] may be called with any ordered sub-sequence
277277
/// of the following snapshots that includes the last one (the one with
278278
/// ConnectionState.done):
279279
///
@@ -284,8 +284,9 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
284284
/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 9)`
285285
/// * `new AsyncSnapshot<int>.withData(ConnectionState.done, 9)`
286286
///
287-
/// The actual sequence of invocations of [build] depends on the relative timing
288-
/// of events produced by the stream and the build rate of the Flutter pipeline.
287+
/// The actual sequence of invocations of the [builder] depends on the relative
288+
/// timing of events produced by the stream and the build rate of the Flutter
289+
/// pipeline.
289290
///
290291
/// Changing the [StreamBuilder] configuration to another stream during event
291292
/// generation introduces snapshot pairs of the form
@@ -303,6 +304,11 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
303304
/// The data and error fields of snapshots produced are only changed when the
304305
/// state is `ConnectionState.active`.
305306
///
307+
/// The initial snapshot data can be controlled by specifying [initialData].
308+
/// You would use this facility to ensure that if the [builder] is invoked
309+
/// before the first event arrives on the stream, the snapshot carries data of
310+
/// your choice rather than the default null value.
311+
///
306312
/// See also:
307313
///
308314
/// * [StreamBuilderBase], which supports widget building based on a computation
@@ -333,19 +339,25 @@ typedef Widget AsyncWidgetBuilder<T>(BuildContext context, AsyncSnapshot<T> snap
333339
class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
334340
/// Creates a new [StreamBuilder] that builds itself based on the latest
335341
/// snapshot of interaction with the specified [stream] and whose build
336-
/// strategy is given by [builder].
342+
/// strategy is given by [builder]. The [initialData] is used to create the
343+
/// initial snapshot. It is null by default.
337344
const StreamBuilder({
338345
Key key,
346+
this.initialData,
339347
Stream<T> stream,
340348
@required this.builder
341-
}) : assert(builder != null),
342-
super(key: key, stream: stream);
349+
})
350+
: assert(builder != null),
351+
super(key: key, stream: stream);
343352

344353
/// The build strategy currently used by this builder. Cannot be null.
345354
final AsyncWidgetBuilder<T> builder;
346355

356+
/// The data that will be used to create the initial snapshot. Null by default.
357+
final T initialData;
358+
347359
@override
348-
AsyncSnapshot<T> initial() => new AsyncSnapshot<T>.nothing(); // ignore: prefer_const_constructors
360+
AsyncSnapshot<T> initial() => new AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
349361

350362
@override
351363
AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
@@ -391,6 +403,11 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
391403
/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
392404
/// * `new AsyncSnapshot<String>.withError(ConnectionState.done, 'some error')`
393405
///
406+
/// The initial snapshot data can be controlled by specifying [initialData]. You
407+
/// would use this facility to ensure that if the [builder] is invoked before
408+
/// the future completes, the snapshot carries data of your choice rather than
409+
/// the default null value.
410+
///
394411
/// The data and error fields of the snapshot change only as the connection
395412
/// state field transitions from `waiting` to `done`, and they will be retained
396413
/// when changing the [FutureBuilder] configuration to another future. If the
@@ -437,6 +454,7 @@ class FutureBuilder<T> extends StatefulWidget {
437454
const FutureBuilder({
438455
Key key,
439456
this.future,
457+
this.initialData,
440458
@required this.builder
441459
}) : assert(builder != null),
442460
super(key: key);
@@ -448,6 +466,9 @@ class FutureBuilder<T> extends StatefulWidget {
448466
/// The build strategy currently used by this builder. Cannot be null.
449467
final AsyncWidgetBuilder<T> builder;
450468

469+
/// The data that will be used to create the initial snapshot. Null by default.
470+
final T initialData;
471+
451472
@override
452473
State<FutureBuilder<T>> createState() => new _FutureBuilderState<T>();
453474
}
@@ -458,11 +479,12 @@ class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
458479
/// calling setState from stale callbacks, e.g. after disposal of this state,
459480
/// or after widget reconfiguration to a new Future.
460481
Object _activeCallbackIdentity;
461-
AsyncSnapshot<T> _snapshot = new AsyncSnapshot<T>.nothing(); // ignore: prefer_const_constructors
482+
AsyncSnapshot<T> _snapshot;
462483

463484
@override
464485
void initState() {
465486
super.initState();
487+
_snapshot = new AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
466488
_subscribe();
467489
}
468490

packages/flutter/test/widgets/async_test.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,34 @@ void main() {
118118
await eventFiring(tester);
119119
expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget);
120120
});
121+
testWidgets('runs the builder using given initial data', (WidgetTester tester) async {
122+
final GlobalKey key = new GlobalKey();
123+
await tester.pumpWidget(new FutureBuilder<String>(
124+
key: key,
125+
future: null,
126+
builder: snapshotText,
127+
initialData: 'I',
128+
));
129+
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget);
130+
});
131+
testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async {
132+
final GlobalKey key = new GlobalKey();
133+
await tester.pumpWidget(new FutureBuilder<String>(
134+
key: key,
135+
future: null,
136+
builder: snapshotText,
137+
initialData: 'I',
138+
));
139+
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget);
140+
final Completer<String> completer = new Completer<String>();
141+
await tester.pumpWidget(new FutureBuilder<String>(
142+
key: key,
143+
future: completer.future,
144+
builder: snapshotText,
145+
initialData: 'Ignored',
146+
));
147+
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
148+
});
121149
});
122150
group('StreamBuilder', () {
123151
testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async {
@@ -180,6 +208,33 @@ void main() {
180208
await eventFiring(tester);
181209
expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget);
182210
});
211+
testWidgets('runs the builder using given initial data', (WidgetTester tester) async {
212+
final StreamController<String> controller = new StreamController<String>();
213+
await tester.pumpWidget(new StreamBuilder<String>(
214+
stream: controller.stream,
215+
builder: snapshotText,
216+
initialData: 'I',
217+
));
218+
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
219+
});
220+
testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async {
221+
final GlobalKey key = new GlobalKey();
222+
await tester.pumpWidget(new StreamBuilder<String>(
223+
key: key,
224+
stream: null,
225+
builder: snapshotText,
226+
initialData: 'I',
227+
));
228+
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget);
229+
final StreamController<String> controller = new StreamController<String>();
230+
await tester.pumpWidget(new StreamBuilder<String>(
231+
key: key,
232+
stream: controller.stream,
233+
builder: snapshotText,
234+
initialData: 'Ignored',
235+
));
236+
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget);
237+
});
183238
});
184239
group('FutureBuilder and StreamBuilder behave identically on Stream from Future', () {
185240
testWidgets('when completing with data', (WidgetTester tester) async {
@@ -211,6 +266,24 @@ void main() {
211266
]));
212267
expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsNWidgets(2));
213268
});
269+
testWidgets('when initialData is used with null Future and Stream', (WidgetTester tester) async {
270+
await tester.pumpWidget(new Column(children: <Widget>[
271+
new FutureBuilder<String>(future: null, builder: snapshotText, initialData: 'I'),
272+
new StreamBuilder<String>(stream: null, builder: snapshotText, initialData: 'I'),
273+
]));
274+
expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsNWidgets(2));
275+
});
276+
testWidgets('when using initialData and completing with data', (WidgetTester tester) async {
277+
final Completer<String> completer = new Completer<String>();
278+
await tester.pumpWidget(new Column(children: <Widget>[
279+
new FutureBuilder<String>(future: completer.future, builder: snapshotText, initialData: 'I'),
280+
new StreamBuilder<String>(stream: completer.future.asStream(), builder: snapshotText, initialData: 'I'),
281+
]));
282+
expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsNWidgets(2));
283+
completer.complete('hello');
284+
await eventFiring(tester);
285+
expect(find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsNWidgets(2));
286+
});
214287
});
215288
group('StreamBuilderBase', () {
216289
testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async {

0 commit comments

Comments
 (0)