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