Minimum reproducible code:
class FooPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncValue = ref.watch(provider1);
print('loading: ${asyncValue.isLoading}, value: ${asyncValue.valueOrNull}');
return Container();
}
}
final provider1 = StreamProvider<int>((ref) {
final stream = ref.watch(provider2);
return stream.maybeWhen(
orElse: () => Stream.value(0),
data: (x) {
print('data = $x');
return Stream.value(x);
},
);
});
final provider2 = StreamProvider<int>((ref) async* {
await Future.delayed(Duration(seconds: 3));
yield 1;
});
Output:
flutter: loading: true, value: null // Why value != 0
flutter: loading: false, value: 0
// After 3 seconds...
flutter: data = 1
flutter: loading: true, value: 0 // Why value != 1
flutter: loading: false, value: 1
There are two questions.
-
When I have already provided a default value in
orElse
callback, then why the first line doesn’t print that value instead of going in the loading state and printingnull
. -
Once I get the data after 3s, why does the provider print
loading: true, value: 0
?
2
Answers
When a
StreamProvider
is first created, it will not have received any data from the stream yet, so itsisLoading
property will betrue
and itsvalueOrNull
property will benull
. This is why the first line of the output prints loading:loading: true, value: null
.Once the stream emits data, the
StreamProvider
will update itsvalueOrNull
property with the latest value from the stream. However, it is possible that theConsumerWidget
has not yet rebuilt to reflect this change. When theConsumerWidget
rebuilds, it will receive the latest value from the stream and print it. This is why the fourth line of the output printsloading: false, value: 1
.The
orElse
callback is used to provide a default value for theStreamProvider
when the stream has not emitted any data yet. It does not affect theStreamProvider
after it has received data from the stream. This is why the second and fifth lines of the output printloading: false, value: 0
.In summary, the
StreamProvider
will initially be in the loading state and have anull
value until it receives data from the stream. Once it has received data, it will update its value to reflect the latest value from the stream, but this update may not be reflected in theConsumerWidget
until it rebuilds. TheorElse
callback only provides a default value for theStreamProvider
when the stream has not emitted any data.provider2
changed (it went from loading to having data). And sinceprovider1
depends on it,provider1
got recomputed.This ends up creating a new stream. The provider then listens to the new stream. Listening to a stream is an async operation, therefore for one frame
provider1
does not have access to the new value exposed by the stream. Hence, you got a loading state.This shouldn’t be a problem though