Skip to content

Commit 4d86f5d

Browse files
authored
Make future and stream required arguments in their respective builder widgets (#125838)
cc'ing existing conversation participants: @domesticmouse @srawlins cc'ing to request review: @goderbauer This PR makes the following constructor arguments required: 1. `FutureBuilder.future` 2. `StreamBuilderBase.stream` 3. `StreamBuilder.stream` This fixes: flutter/flutter#83081 flutter/flutter#125188 (dupe of 83081) This obviates: https://github.com/dart-lang/linter/issues/4309 (I suggest we skip straight to merging this PR as this should be a low impact breaking change-assuming few to no devs are intentionally using the builders without their relevant arguments, however we could always merge 4309 first and then this) flutter/flutter#83101 (The above PR required that at least one of future and initial data be non-null, this is undesirable as there are plenty of valid reasons to have both arguments be null) See above issues for a deeper dive, but here is a summary: It is very easy for a developer to forget to specify `future` or `stream` when using the respective `*Builder` widgets. This produces a non-obvious failure where the UI sits in a "no data yet received" state. It is easy for a dev to misinterpret this as the async work backing the future/stream hanging and they thus waste a lot of time trying to debug the async work. As such, we should require these two constructor arguments to make it impossible/much harder for devs to make this time-wasting mistake. This is a breaking change. However, it should break only a small number of active projects given that using a builder without specifying `future` or `stream` seems highly niche. The only place I've found non-accidental examples of this is in widget tests where you're calling `pumpWidget` with and without these arguments to test `*Builder.didUpdateWidget`'s behavior. In this and similar cases, it is a trivial fix to add `future: null`/`stream: null`. *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
1 parent 7907f93 commit 4d86f5d

File tree

2 files changed

+14
-11
lines changed

2 files changed

+14
-11
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import 'framework.dart';
4040
/// recent interaction is needed for widget building.
4141
abstract class StreamBuilderBase<T, S> extends StatefulWidget {
4242
/// Creates a [StreamBuilderBase] connected to the specified [stream].
43-
const StreamBuilderBase({ super.key, this.stream });
43+
const StreamBuilderBase({ super.key, required this.stream });
4444

4545
/// The asynchronous computation to which this builder is currently connected,
4646
/// possibly null. When changed, the current summary is updated using
@@ -391,7 +391,7 @@ class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
391391
const StreamBuilder({
392392
super.key,
393393
this.initialData,
394-
super.stream,
394+
required super.stream,
395395
required this.builder,
396396
});
397397

@@ -521,7 +521,7 @@ class FutureBuilder<T> extends StatefulWidget {
521521
/// The [builder] must not be null.
522522
const FutureBuilder({
523523
super.key,
524-
this.future,
524+
required this.future,
525525
this.initialData,
526526
required this.builder,
527527
});

packages/flutter/test/widgets/async_test.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ void main() {
105105
testWidgets('gracefully handles transition from null future', (WidgetTester tester) async {
106106
final GlobalKey key = GlobalKey();
107107
await tester.pumpWidget(FutureBuilder<String>(
108-
key: key, builder: snapshotText,
108+
key: key, builder: snapshotText, future: null,
109109
));
110110
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
111111
final Completer<String> completer = Completer<String>();
@@ -122,7 +122,7 @@ void main() {
122122
));
123123
expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
124124
await tester.pumpWidget(FutureBuilder<String>(
125-
key: key, builder: snapshotText,
125+
key: key, builder: snapshotText, future: null,
126126
));
127127
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
128128
completer.complete('hello');
@@ -170,6 +170,7 @@ void main() {
170170
final GlobalKey key = GlobalKey();
171171
await tester.pumpWidget(FutureBuilder<String>(
172172
key: key,
173+
future: null,
173174
builder: snapshotText,
174175
initialData: 'I',
175176
));
@@ -179,6 +180,7 @@ void main() {
179180
final GlobalKey key = GlobalKey();
180181
await tester.pumpWidget(FutureBuilder<String>(
181182
key: key,
183+
future: null,
182184
builder: snapshotText,
183185
initialData: 'I',
184186
));
@@ -215,7 +217,7 @@ void main() {
215217
testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async {
216218
final GlobalKey key = GlobalKey();
217219
await tester.pumpWidget(StreamBuilder<String>(
218-
key: key, builder: snapshotText,
220+
key: key, builder: snapshotText, stream: null,
219221
));
220222
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
221223
final StreamController<String> controller = StreamController<String>();
@@ -232,7 +234,7 @@ void main() {
232234
));
233235
expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
234236
await tester.pumpWidget(StreamBuilder<String>(
235-
key: key, builder: snapshotText,
237+
key: key, builder: snapshotText, stream: null,
236238
));
237239
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
238240
});
@@ -285,6 +287,7 @@ void main() {
285287
final GlobalKey key = GlobalKey();
286288
await tester.pumpWidget(StreamBuilder<String>(
287289
key: key,
290+
stream: null,
288291
builder: snapshotText,
289292
initialData: 'I',
290293
));
@@ -335,15 +338,15 @@ void main() {
335338
});
336339
testWidgets('when Future is null', (WidgetTester tester) async {
337340
await tester.pumpWidget(Column(children: <Widget>[
338-
FutureBuilder<String>(builder: snapshotText),
339-
StreamBuilder<String>(builder: snapshotText),
341+
FutureBuilder<String>(builder: snapshotText, future: null),
342+
StreamBuilder<String>(builder: snapshotText, stream: null,),
340343
]));
341344
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsNWidgets(2));
342345
});
343346
testWidgets('when initialData is used with null Future and Stream', (WidgetTester tester) async {
344347
await tester.pumpWidget(Column(children: <Widget>[
345-
FutureBuilder<String>(builder: snapshotText, initialData: 'I'),
346-
StreamBuilder<String>(builder: snapshotText, initialData: 'I'),
348+
FutureBuilder<String>(builder: snapshotText, initialData: 'I', future: null),
349+
StreamBuilder<String>(builder: snapshotText, initialData: 'I', stream: null),
347350
]));
348351
expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsNWidgets(2));
349352
});

0 commit comments

Comments
 (0)