• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// Widgets that handle interaction with asynchronous computations.
6///
7/// Asynchronous computations are represented by [Future]s and [Stream]s.
8
9import 'dart:async' show Future, Stream, StreamSubscription;
10
11import 'framework.dart';
12
13// Examples can assume:
14// dynamic _lot;
15// Future<String> _calculation;
16
17/// Base class for widgets that build themselves based on interaction with
18/// a specified [Stream].
19///
20/// A [StreamBuilderBase] is stateful and maintains a summary of the interaction
21/// so far. The type of the summary and how it is updated with each interaction
22/// is defined by sub-classes.
23///
24/// Examples of summaries include:
25///
26/// * the running average of a stream of integers;
27/// * the current direction and speed based on a stream of geolocation data;
28/// * a graph displaying data points from a stream.
29///
30/// In general, the summary is the result of a fold computation over the data
31/// items and errors received from the stream along with pseudo-events
32/// representing termination or change of stream. The initial summary is
33/// specified by sub-classes by overriding [initial]. The summary updates on
34/// receipt of stream data and errors are specified by overriding [afterData] and
35/// [afterError], respectively. If needed, the summary may be updated on stream
36/// termination by overriding [afterDone]. Finally, the summary may be updated
37/// on change of stream by overriding [afterDisconnected] and [afterConnected].
38///
39/// `T` is the type of stream events.
40///
41/// `S` is the type of interaction summary.
42///
43/// See also:
44///
45///  * [StreamBuilder], which is specialized for the case where only the most
46///    recent interaction is needed for widget building.
47abstract class StreamBuilderBase<T, S> extends StatefulWidget {
48  /// Creates a [StreamBuilderBase] connected to the specified [stream].
49  const StreamBuilderBase({ Key key, this.stream }) : super(key: key);
50
51  /// The asynchronous computation to which this builder is currently connected,
52  /// possibly null. When changed, the current summary is updated using
53  /// [afterDisconnected], if the previous stream was not null, followed by
54  /// [afterConnected], if the new stream is not null.
55  final Stream<T> stream;
56
57  /// Returns the initial summary of stream interaction, typically representing
58  /// the fact that no interaction has happened at all.
59  ///
60  /// Sub-classes must override this method to provide the initial value for
61  /// the fold computation.
62  S initial();
63
64  /// Returns an updated version of the [current] summary reflecting that we
65  /// are now connected to a stream.
66  ///
67  /// The default implementation returns [current] as is.
68  S afterConnected(S current) => current;
69
70  /// Returns an updated version of the [current] summary following a data event.
71  ///
72  /// Sub-classes must override this method to specify how the current summary
73  /// is combined with the new data item in the fold computation.
74  S afterData(S current, T data);
75
76  /// Returns an updated version of the [current] summary following an error.
77  ///
78  /// The default implementation returns [current] as is.
79  S afterError(S current, Object error) => current;
80
81  /// Returns an updated version of the [current] summary following stream
82  /// termination.
83  ///
84  /// The default implementation returns [current] as is.
85  S afterDone(S current) => current;
86
87  /// Returns an updated version of the [current] summary reflecting that we
88  /// are no longer connected to a stream.
89  ///
90  /// The default implementation returns [current] as is.
91  S afterDisconnected(S current) => current;
92
93  /// Returns a Widget based on the [currentSummary].
94  Widget build(BuildContext context, S currentSummary);
95
96  @override
97  State<StreamBuilderBase<T, S>> createState() => _StreamBuilderBaseState<T, S>();
98}
99
100/// State for [StreamBuilderBase].
101class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
102  StreamSubscription<T> _subscription;
103  S _summary;
104
105  @override
106  void initState() {
107    super.initState();
108    _summary = widget.initial();
109    _subscribe();
110  }
111
112  @override
113  void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
114    super.didUpdateWidget(oldWidget);
115    if (oldWidget.stream != widget.stream) {
116      if (_subscription != null) {
117        _unsubscribe();
118        _summary = widget.afterDisconnected(_summary);
119      }
120      _subscribe();
121    }
122  }
123
124  @override
125  Widget build(BuildContext context) => widget.build(context, _summary);
126
127  @override
128  void dispose() {
129    _unsubscribe();
130    super.dispose();
131  }
132
133  void _subscribe() {
134    if (widget.stream != null) {
135      _subscription = widget.stream.listen((T data) {
136        setState(() {
137          _summary = widget.afterData(_summary, data);
138        });
139      }, onError: (Object error) {
140        setState(() {
141          _summary = widget.afterError(_summary, error);
142        });
143      }, onDone: () {
144        setState(() {
145          _summary = widget.afterDone(_summary);
146        });
147      });
148      _summary = widget.afterConnected(_summary);
149    }
150  }
151
152  void _unsubscribe() {
153    if (_subscription != null) {
154      _subscription.cancel();
155      _subscription = null;
156    }
157  }
158}
159
160/// The state of connection to an asynchronous computation.
161///
162/// See also:
163///
164///  * [AsyncSnapshot], which augments a connection state with information
165///    received from the asynchronous computation.
166enum ConnectionState {
167  /// Not currently connected to any asynchronous computation.
168  ///
169  /// For example, a [FutureBuilder] whose [FutureBuilder.future] is null.
170  none,
171
172  /// Connected to an asynchronous computation and awaiting interaction.
173  waiting,
174
175  /// Connected to an active asynchronous computation.
176  ///
177  /// For example, a [Stream] that has returned at least one value, but is not
178  /// yet done.
179  active,
180
181  /// Connected to a terminated asynchronous computation.
182  done,
183}
184
185/// Immutable representation of the most recent interaction with an asynchronous
186/// computation.
187///
188/// See also:
189///
190///  * [StreamBuilder], which builds itself based on a snapshot from interacting
191///    with a [Stream].
192///  * [FutureBuilder], which builds itself based on a snapshot from interacting
193///    with a [Future].
194@immutable
195class AsyncSnapshot<T> {
196  /// Creates an [AsyncSnapshot] with the specified [connectionState],
197  /// and optionally either [data] or [error] (but not both).
198  const AsyncSnapshot._(this.connectionState, this.data, this.error)
199    : assert(connectionState != null),
200      assert(!(data != null && error != null));
201
202  /// Creates an [AsyncSnapshot] in [ConnectionState.none] with null data and error.
203  const AsyncSnapshot.nothing() : this._(ConnectionState.none, null, null);
204
205  /// Creates an [AsyncSnapshot] in the specified [state] and with the specified [data].
206  const AsyncSnapshot.withData(ConnectionState state, T data) : this._(state, data, null);
207
208  /// Creates an [AsyncSnapshot] in the specified [state] and with the specified [error].
209  const AsyncSnapshot.withError(ConnectionState state, Object error) : this._(state, null, error);
210
211  /// Current state of connection to the asynchronous computation.
212  final ConnectionState connectionState;
213
214  /// The latest data received by the asynchronous computation.
215  ///
216  /// If this is non-null, [hasData] will be true.
217  ///
218  /// If [error] is not null, this will be null. See [hasError].
219  ///
220  /// If the asynchronous computation has never returned a value, this may be
221  /// set to an initial data value specified by the relevant widget. See
222  /// [FutureBuilder.initialData] and [StreamBuilder.initialData].
223  final T data;
224
225  /// Returns latest data received, failing if there is no data.
226  ///
227  /// Throws [error], if [hasError]. Throws [StateError], if neither [hasData]
228  /// nor [hasError].
229  T get requireData {
230    if (hasData)
231      return data;
232    if (hasError)
233      throw error;
234    throw StateError('Snapshot has neither data nor error');
235  }
236
237  /// The latest error object received by the asynchronous computation.
238  ///
239  /// If this is non-null, [hasError] will be true.
240  ///
241  /// If [data] is not null, this will be null.
242  final Object error;
243
244  /// Returns a snapshot like this one, but in the specified [state].
245  ///
246  /// The [data] and [error] fields persist unmodified, even if the new state is
247  /// [ConnectionState.none].
248  AsyncSnapshot<T> inState(ConnectionState state) => AsyncSnapshot<T>._(state, data, error);
249
250  /// Returns whether this snapshot contains a non-null [data] value.
251  ///
252  /// This can be false even when the asynchronous computation has completed
253  /// successfully, if the computation did not return a non-null value. For
254  /// example, a [Future<void>] will complete with the null value even if it
255  /// completes successfully.
256  bool get hasData => data != null;
257
258  /// Returns whether this snapshot contains a non-null [error] value.
259  ///
260  /// This is always true if the asynchronous computation's last result was
261  /// failure.
262  bool get hasError => error != null;
263
264  @override
265  String toString() => '$runtimeType($connectionState, $data, $error)';
266
267  @override
268  bool operator ==(dynamic other) {
269    if (identical(this, other))
270      return true;
271    if (other is! AsyncSnapshot<T>)
272      return false;
273    final AsyncSnapshot<T> typedOther = other;
274    return connectionState == typedOther.connectionState
275        && data == typedOther.data
276        && error == typedOther.error;
277  }
278
279  @override
280  int get hashCode => hashValues(connectionState, data, error);
281}
282
283/// Signature for strategies that build widgets based on asynchronous
284/// interaction.
285///
286/// See also:
287///
288///  * [StreamBuilder], which delegates to an [AsyncWidgetBuilder] to build
289///    itself based on a snapshot from interacting with a [Stream].
290///  * [FutureBuilder], which delegates to an [AsyncWidgetBuilder] to build
291///    itself based on a snapshot from interacting with a [Future].
292typedef AsyncWidgetBuilder<T> = Widget Function(BuildContext context, AsyncSnapshot<T> snapshot);
293
294/// Widget that builds itself based on the latest snapshot of interaction with
295/// a [Stream].
296///
297/// {@youtube 560 315 https://www.youtube.com/watch?v=MkKEWHfy99Y}
298///
299/// Widget rebuilding is scheduled by each interaction, using [State.setState],
300/// but is otherwise decoupled from the timing of the stream. The [builder]
301/// is called at the discretion of the Flutter pipeline, and will thus receive a
302/// timing-dependent sub-sequence of the snapshots that represent the
303/// interaction with the stream.
304///
305/// As an example, when interacting with a stream producing the integers
306/// 0 through 9, the [builder] may be called with any ordered sub-sequence
307/// of the following snapshots that includes the last one (the one with
308/// ConnectionState.done):
309///
310/// * `new AsyncSnapshot<int>.withData(ConnectionState.waiting, null)`
311/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 0)`
312/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 1)`
313/// * ...
314/// * `new AsyncSnapshot<int>.withData(ConnectionState.active, 9)`
315/// * `new AsyncSnapshot<int>.withData(ConnectionState.done, 9)`
316///
317/// The actual sequence of invocations of the [builder] depends on the relative
318/// timing of events produced by the stream and the build rate of the Flutter
319/// pipeline.
320///
321/// Changing the [StreamBuilder] configuration to another stream during event
322/// generation introduces snapshot pairs of the form:
323///
324/// * `new AsyncSnapshot<int>.withData(ConnectionState.none, 5)`
325/// * `new AsyncSnapshot<int>.withData(ConnectionState.waiting, 5)`
326///
327/// The latter will be produced only when the new stream is non-null, and the
328/// former only when the old stream is non-null.
329///
330/// The stream may produce errors, resulting in snapshots of the form:
331///
332/// * `new AsyncSnapshot<int>.withError(ConnectionState.active, 'some error')`
333///
334/// The data and error fields of snapshots produced are only changed when the
335/// state is `ConnectionState.active`.
336///
337/// The initial snapshot data can be controlled by specifying [initialData].
338/// This should be used to ensure that the first frame has the expected value,
339/// as the builder will always be called before the stream listener has a chance
340/// to be processed.
341///
342/// {@tool sample}
343///
344/// This sample shows a [StreamBuilder] configuring a text label to show the
345/// latest bid received for a lot in an auction. Assume the `_lot` field is
346/// set by a selector elsewhere in the UI.
347///
348/// ```dart
349/// StreamBuilder<int>(
350///   stream: _lot?.bids, // a Stream<int> or null
351///   builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
352///     if (snapshot.hasError)
353///       return Text('Error: ${snapshot.error}');
354///     switch (snapshot.connectionState) {
355///       case ConnectionState.none: return Text('Select lot');
356///       case ConnectionState.waiting: return Text('Awaiting bids...');
357///       case ConnectionState.active: return Text('\$${snapshot.data}');
358///       case ConnectionState.done: return Text('\$${snapshot.data} (closed)');
359///     }
360///     return null; // unreachable
361///   },
362/// )
363/// ```
364/// {@end-tool}
365///
366/// See also:
367///
368///  * [ValueListenableBuilder], which wraps a [ValueListenable] instead of a
369///    [Stream].
370///  * [StreamBuilderBase], which supports widget building based on a computation
371///    that spans all interactions made with the stream.
372// TODO(ianh): remove unreachable code above once https://github.com/dart-lang/linter/issues/1139 is fixed
373class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
374  /// Creates a new [StreamBuilder] that builds itself based on the latest
375  /// snapshot of interaction with the specified [stream] and whose build
376  /// strategy is given by [builder].
377  ///
378  /// The [initialData] is used to create the initial snapshot.
379  ///
380  /// The [builder] must not be null.
381  const StreamBuilder({
382    Key key,
383    this.initialData,
384    Stream<T> stream,
385    @required this.builder,
386  }) : assert(builder != null),
387       super(key: key, stream: stream);
388
389  /// The build strategy currently used by this builder.
390  final AsyncWidgetBuilder<T> builder;
391
392  /// The data that will be used to create the initial snapshot.
393  ///
394  /// Providing this value (presumably obtained synchronously somehow when the
395  /// [Stream] was created) ensures that the first frame will show useful data.
396  /// Otherwise, the first frame will be built with the value null, regardless
397  /// of whether a value is available on the stream: since streams are
398  /// asynchronous, no events from the stream can be obtained before the initial
399  /// build.
400  final T initialData;
401
402  @override
403  AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
404
405  @override
406  AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
407
408  @override
409  AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
410    return AsyncSnapshot<T>.withData(ConnectionState.active, data);
411  }
412
413  @override
414  AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error) {
415    return AsyncSnapshot<T>.withError(ConnectionState.active, error);
416  }
417
418  @override
419  AsyncSnapshot<T> afterDone(AsyncSnapshot<T> current) => current.inState(ConnectionState.done);
420
421  @override
422  AsyncSnapshot<T> afterDisconnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.none);
423
424  @override
425  Widget build(BuildContext context, AsyncSnapshot<T> currentSummary) => builder(context, currentSummary);
426}
427
428/// Widget that builds itself based on the latest snapshot of interaction with
429/// a [Future].
430///
431/// The [future] must have been obtained earlier, e.g. during [State.initState],
432/// [State.didUpdateConfig], or [State.didChangeDependencies]. It must not be
433/// created during the [State.build] or [StatelessWidget.build] method call when
434/// constructing the [FutureBuilder]. If the [future] is created at the same
435/// time as the [FutureBuilder], then every time the [FutureBuilder]'s parent is
436/// rebuilt, the asynchronous task will be restarted.
437///
438/// A general guideline is to assume that every `build` method could get called
439/// every frame, and to treat omitted calls as an optimization.
440///
441/// {@youtube 560 315 https://www.youtube.com/watch?v=ek8ZPdWj4Qo}
442///
443/// ## Timing
444///
445/// Widget rebuilding is scheduled by the completion of the future, using
446/// [State.setState], but is otherwise decoupled from the timing of the future.
447/// The [builder] callback is called at the discretion of the Flutter pipeline, and
448/// will thus receive a timing-dependent sub-sequence of the snapshots that
449/// represent the interaction with the future.
450///
451/// A side-effect of this is that providing a new but already-completed future
452/// to a [FutureBuilder] will result in a single frame in the
453/// [ConnectionState.waiting] state. This is because there is no way to
454/// synchronously determine that a [Future] has already completed.
455///
456/// ## Builder contract
457///
458/// For a future that completes successfully with data, assuming [initialData]
459/// is null, the [builder] will be called with either both or only the latter of
460/// the following snapshots:
461///
462/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
463/// * `new AsyncSnapshot<String>.withData(ConnectionState.done, 'some data')`
464///
465/// If that same future instead completed with an error, the [builder] would be
466/// called with either both or only the latter of:
467///
468/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, null)`
469/// * `new AsyncSnapshot<String>.withError(ConnectionState.done, 'some error')`
470///
471/// The initial snapshot data can be controlled by specifying [initialData]. You
472/// would use this facility to ensure that if the [builder] is invoked before
473/// the future completes, the snapshot carries data of your choice rather than
474/// the default null value.
475///
476/// The data and error fields of the snapshot change only as the connection
477/// state field transitions from `waiting` to `done`, and they will be retained
478/// when changing the [FutureBuilder] configuration to another future. If the
479/// old future has already completed successfully with data as above, changing
480/// configuration to a new future results in snapshot pairs of the form:
481///
482/// * `new AsyncSnapshot<String>.withData(ConnectionState.none, 'data of first future')`
483/// * `new AsyncSnapshot<String>.withData(ConnectionState.waiting, 'data of second future')`
484///
485/// In general, the latter will be produced only when the new future is
486/// non-null, and the former only when the old future is non-null.
487///
488/// A [FutureBuilder] behaves identically to a [StreamBuilder] configured with
489/// `future?.asStream()`, except that snapshots with `ConnectionState.active`
490/// may appear for the latter, depending on how the stream is implemented.
491///
492/// {@tool sample}
493///
494/// This sample shows a [FutureBuilder] configuring a text label to show the
495/// state of an asynchronous calculation returning a string. Assume the
496/// `_calculation` field is set by pressing a button elsewhere in the UI.
497///
498/// ```dart
499/// FutureBuilder<String>(
500///   future: _calculation, // a previously-obtained Future<String> or null
501///   builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
502///     switch (snapshot.connectionState) {
503///       case ConnectionState.none:
504///         return Text('Press button to start.');
505///       case ConnectionState.active:
506///       case ConnectionState.waiting:
507///         return Text('Awaiting result...');
508///       case ConnectionState.done:
509///         if (snapshot.hasError)
510///           return Text('Error: ${snapshot.error}');
511///         return Text('Result: ${snapshot.data}');
512///     }
513///     return null; // unreachable
514///   },
515/// )
516/// ```
517/// {@end-tool}
518// TODO(ianh): remove unreachable code above once https://github.com/dart-lang/linter/issues/1141 is fixed
519class FutureBuilder<T> extends StatefulWidget {
520  /// Creates a widget that builds itself based on the latest snapshot of
521  /// interaction with a [Future].
522  ///
523  /// The [builder] must not be null.
524  const FutureBuilder({
525    Key key,
526    this.future,
527    this.initialData,
528    @required this.builder,
529  }) : assert(builder != null),
530       super(key: key);
531
532  /// The asynchronous computation to which this builder is currently connected,
533  /// possibly null.
534  ///
535  /// If no future has yet completed, including in the case where [future] is
536  /// null, the data provided to the [builder] will be set to [initialData].
537  final Future<T> future;
538
539  /// The build strategy currently used by this builder.
540  ///
541  /// The builder is provided with an [AsyncSnapshot] object whose
542  /// [AsyncSnapshot.connectionState] property will be one of the following
543  /// values:
544  ///
545  ///  * [ConnectionState.none]: [future] is null. The [AsyncSnapshot.data] will
546  ///    be set to [initialData], unless a future has previously completed, in
547  ///    which case the previous result persists.
548  ///
549  ///  * [ConnectionState.waiting]: [future] is not null, but has not yet
550  ///    completed. The [AsyncSnapshot.data] will be set to [initialData],
551  ///    unless a future has previously completed, in which case the previous
552  ///    result persists.
553  ///
554  ///  * [ConnectionState.done]: [future] is not null, and has completed. If the
555  ///    future completed successfully, the [AsyncSnapshot.data] will be set to
556  ///    the value to which the future completed. If it completed with an error,
557  ///    [AsyncSnapshot.hasError] will be true and [AsyncSnapshot.error] will be
558  ///    set to the error object.
559  final AsyncWidgetBuilder<T> builder;
560
561  /// The data that will be used to create the snapshots provided until a
562  /// non-null [future] has completed.
563  ///
564  /// If the future completes with an error, the data in the [AsyncSnapshot]
565  /// provided to the [builder] will become null, regardless of [initialData].
566  /// (The error itself will be available in [AsyncSnapshot.error], and
567  /// [AsyncSnapshot.hasError] will be true.)
568  final T initialData;
569
570  @override
571  State<FutureBuilder<T>> createState() => _FutureBuilderState<T>();
572}
573
574/// State for [FutureBuilder].
575class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
576  /// An object that identifies the currently active callbacks. Used to avoid
577  /// calling setState from stale callbacks, e.g. after disposal of this state,
578  /// or after widget reconfiguration to a new Future.
579  Object _activeCallbackIdentity;
580  AsyncSnapshot<T> _snapshot;
581
582  @override
583  void initState() {
584    super.initState();
585    _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
586    _subscribe();
587  }
588
589  @override
590  void didUpdateWidget(FutureBuilder<T> oldWidget) {
591    super.didUpdateWidget(oldWidget);
592    if (oldWidget.future != widget.future) {
593      if (_activeCallbackIdentity != null) {
594        _unsubscribe();
595        _snapshot = _snapshot.inState(ConnectionState.none);
596      }
597      _subscribe();
598    }
599  }
600
601  @override
602  Widget build(BuildContext context) => widget.builder(context, _snapshot);
603
604  @override
605  void dispose() {
606    _unsubscribe();
607    super.dispose();
608  }
609
610  void _subscribe() {
611    if (widget.future != null) {
612      final Object callbackIdentity = Object();
613      _activeCallbackIdentity = callbackIdentity;
614      widget.future.then<void>((T data) {
615        if (_activeCallbackIdentity == callbackIdentity) {
616          setState(() {
617            _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
618          });
619        }
620      }, onError: (Object error) {
621        if (_activeCallbackIdentity == callbackIdentity) {
622          setState(() {
623            _snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
624          });
625        }
626      });
627      _snapshot = _snapshot.inState(ConnectionState.waiting);
628    }
629  }
630
631  void _unsubscribe() {
632    _activeCallbackIdentity = null;
633  }
634}
635