riverpod
riverpod copied to clipboard
Watching selectAsync within a provider chain never completes
This one is a little difficult to put into words. I have a scenario where a widget watches a provider that uses a chain of providers. Within on of these providers, a selectAsync
is being watched but never produces a value. However, when reading the selectAsync
or when reading/watching the future
, the provider produces a value. This isn't desirable in all cases though.
Any help would be appreciated, thank you!
lib/providers.dart
in the attached sample contains the gist of the issue:
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
@riverpod
Future<int> value0(Value0Ref ref) async {
debugPrint('value0');
await Future.delayed(const Duration(milliseconds: 1500));
return 0;
}
@riverpod
Future<int> value1(Value1Ref ref) async {
debugPrint('value1');
// ISSUE: When watching 'selectAsync', the future never completes.
return await ref.watch(value0Provider.selectAsync((data) => data));
// However, if we read instead of watch, the future does complete.
// return await ref.read(value0Provider.selectAsync((data) => data));
// Also, if we 'read' or 'watch' 'future' instead of using 'selectAsync', the future also completes.
// return await ref.read(value0Provider.future);
// return await ref.watch(value0Provider.future);
}
@riverpod
Future<int> value2(Value2Ref ref) async {
debugPrint('value2');
return await ref.read(value1Provider.future);
}
@riverpod
Future<int> value3(Value3Ref ref) async {
debugPrint('value3');
return await ref.read(value2Provider.future);
}
To Reproduce Run the following sample flutter project. riverpod_issue.zip
Expected behavior
The expectation is the widget should update after loading is complete, but never does. See the comments in lib/provider.dart
for what does make it work.
I'm currently in the process of investigating this. Thanks for the reproducible example!
So far I haven't found what exactly is causing this. But somehow in your example, value1Provider
gets disposed even though it shouldn't and it enters a bad state. From there on, it's considered as not watching anything (due to listeners being cleared since the provider was disposed). So "selectAsync" never receives the updated value
It's a tricky one. I'll need a bit more time to investigate.
Hi,
I've also bumped into this odd behavior. In my case, I was trying to call await ref.read(value1Provider.future);
to get a value from inside a button's onPressed
method. That value1Provider watches another provider using .selectAsync inside its build method. Oddly enough, this seems to work when using .watch
instead of .read
(but that is a bad practice). It also seems to affect stream providers.
Difficult to put into words indeed.
I'm sharing a bit of code in hope it helps:
Code
Future Providers:
@Riverpod(keepAlive: false)
Future<String> futureValue1(FutureValue1Ref ref) async {
ref.onDispose(() {
debugPrint('Disposed futureValue1Provider ${ref.state}');
});
await Future.delayed(const Duration(seconds: 2));
final value = 'futureValue1 calculated (${DateTime.now()})';
debugPrint(value);
return value;
}
@Riverpod(keepAlive: false)
Future<String> futureValue2(FutureValue2Ref ref) async {
ref.onDispose(() {
debugPrint('Disposed futureValue2Provider ${ref.state}');
});
await Future.delayed(const Duration(seconds: 3));
final value = 'futureValue2 calculated (${DateTime.now()})';
debugPrint(value);
return value;
}
@Riverpod(keepAlive: false)
FutureOr<String> combineFutureValues(CombineFutureValuesRef ref) async {
ref.onDispose(() {
debugPrint('Disposed combineFutureValuesProvider ${ref.state}');
});
// ISSUE: This does not work. (works when using .watch(.future) instead of .read(.future) inside button.onPressed)
final value1 =
await ref.watch(futureValue1Provider.selectAsync((data) => data));
final value2 =
await ref.watch(futureValue2Provider.selectAsync((data) => data));
// This seems to work, but combineFutureValues is not disposed after .read call ends (is this normal?).
// final value1 = await ref.watch(futureValue1Provider.future);
// final value2 =
// await ref.watch(futureValue2Provider.selectAsync((data) => data));
// This also works, but combineFutureValues is not disposed after .read call ends (is this normal?).
// final value1 = await ref.watch(futureValue1Provider.future);
// final value2 = await ref.watch(futureValue2Provider.future);
return '$value1 + $value2';
}
Stream Providers:
@Riverpod(keepAlive: false)
Stream<String> streamValue1(StreamValue1Ref ref) async* {
ref.onDispose(() {
debugPrint('Disposed streamValue1Provider ${ref.state}');
});
String value;
await Future.delayed(const Duration(seconds: 2));
value = 'streamValue1.1 calculated ${DateTime.now()}';
debugPrint(value);
yield value;
await Future.delayed(const Duration(seconds: 5));
value = 'streamValue1.2 calculated ${DateTime.now()}';
debugPrint(value);
yield value;
}
@Riverpod(keepAlive: false)
Stream<String> streamValue2(StreamValue2Ref ref) async* {
ref.onDispose(() {
debugPrint('Disposed streamValue2Provider');
});
String value;
await Future.delayed(const Duration(seconds: 4));
value = 'streamValue2.1 calculated ${DateTime.now()}';
debugPrint(value);
yield value;
}
@Riverpod(keepAlive: false)
FutureOr<String> combineStreamValues(CombineStreamValuesRef ref) async {
ref.onDispose(() {
debugPrint('Disposed combineStreamValues ${ref.state}');
});
// ISSUE: This does not work.
final value1 =
await ref.watch(streamValue1Provider.selectAsync((data) => data));
final value2 =
await ref.watch(streamValue2Provider.selectAsync((data) => data));
// This works.
// final value1 = await ref.watch(value1Provider.future);
// final value2 = await ref.watch(value2Provider.future);
return '$value1 + $value2';
}
Widgets:
Consumer(
builder: (context, ref, child) {
return Column(
children: [
TextButton(
child: Text("Combine stream values"),
onPressed: () async {
// ISSUE: This never gets called when using .selectAsync
final String combineStreamValues = await ref
.read(combineStreamValuesProvider.future);
print(
'combineStreamValuesProvider.future result: $combineStreamValues | (${DateTime.now()})');
},
),
TextButton(
child: Text("Combine stream values with .watch"),
onPressed: () async {
// Using .watch works when using .selectAsync, but shouldn't be used here.
final String combineStreamValues = await ref
.watch(combineStreamValuesProvider.future);
print(
'combineStreamValuesProvider.future result: $combineStreamValues | (${DateTime.now()})');
},
),
TextButton(
child: Text("Combine future values with .read"),
onPressed: () async {
final String combineFutureValues = await ref
.read(combineFutureValuesProvider.future);
// ISSUE: This never gets called when using .selectAsync
print(
'combineFutureValuesProvider.future result: $combineFutureValues | (${DateTime.now()})');
},
),
TextButton(
child: Text("Combine future values with .watch"),
onPressed: () async {
// Using .watch works when using .selectAsync, but shouldn't be used here
final String combineFutureValues = await ref
.watch(combineFutureValuesProvider.future);
print(
'combineFutureValuesProvider.future result: $combineFutureValues | (${DateTime.now()})');
},
),
],
);
},
)
Configuration as tested
dependencies:
flutter_riverpod: ^3.0.0-dev.3
riverpod_annotation: ^3.0.0-dev.3
dev_dependencies:
build_runner: ^2.4.7
riverpod_generator: ^3.0.0-dev.11
riverpod_lint: ^3.0.0-dev.4
custom_lint: ^0.5.7
Others:
Flutter (Channel stable, 3.16.5, on macOS 14.2 23C64 darwin-arm64, locale en-BR)
Android toolchain - develop for Android devices (Android SDK version 34.0.0)
Xcode - develop for iOS and macOS (Xcode 15.0.1)
Android Studio (version 2022.3)
VS Code (version 1.85.1)
Thanks!
Is there any workaround for this? Watching future providers seems like very basic functionality that will be present in many projects so it's a real pain if it's broken. Currently, it seems like the only reliable way to watch a future provider is to use ref.watch(someProvider.future)
but this causes rebuilds when the value doesn't change.
On a side note, I feel like the default behavior of ref.watch(someProvider.future)
should be ref.watch(someProvider.selectAsync((data) => data))
. In most cases. when something watches a future provider it wants to watch the value, not the future. Typically, it wants to await for the first value, then receive updates when the value changes. As an alternative, I wonder if there could be a method added like ref.watch(someProvider.selectValue)
which is just a shortcut for ref.watch(someProvider.selectAsync((data) => data))
. This way you don't have to add that anonymous function every time.
Hi everyone, Came across this issue yesterday and made a simple reproduction example of my usecase.
Code
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const ProviderScope(
child: MaterialApp(
home: MainPage(title: 'Riverpod test'),
),
);
}
}
class MainPage extends ConsumerWidget {
const MainPage({required this.title, super.key});
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () async {
final a = await ref.read(aProvider.future);
log(a, name: 'A');
},
child: const Text('A'),
),
ElevatedButton(
onPressed: () async {
final b = await ref.read(bProvider.future);
log(b, name: 'B');
},
child: const Text('B'),
),
ElevatedButton(
onPressed: () async {
final c = await ref.read(watchDatabaseProvider.selectAsync((id) => '$id+456'));
log(c, name: 'C');
},
child: const Text('C'),
),
],
),
),
);
}
}
// Provider using `.selectAsync`
@riverpod
Future<String> a(ARef ref) async {
return ref.watch(watchDatabaseProvider.selectAsync((id) => '$id+456'));
}
// Provider using `.future`
@riverpod
Future<String> b(BRef ref) async {
final id = await ref.watch(watchDatabaseProvider.future);
return '$id+456';
}
@riverpod
Stream<String> watchDatabase(WatchDatabaseRef ref) => databaseStream;
Stream<String> get databaseStream async* {
// Mock DB query delay
await Future.delayed(const Duration(milliseconds: 100));
yield '123';
}
Config
dependencies:
flutter_riverpod: ^2.4.10
riverpod_annotation: ^2.3.4
dev_dependencies:
build_runner: ^2.4.8
riverpod_generator: ^2.3.11
All cases are using the data exposed by watchDatabaseProvider
, which is a StreamProvider
.
Cases A and B are almost similar, but differ in the way they read watchDatabaseProvider
:
-
A use
.selectAsync
on an intermediateFutureProvider
and never resolves ❌ -
B use
.future
on an intermediateFutureProvider
and resolve as expected ✅ -
C directly read the
watchDatabaseProvider
using a.selectAsync
and resolves as expected ✅
When playing with the mock delay on watchDatabaseProvider
, when going under ~10ms, case A starts to resolve correctly (but not every time).
Note: The results are exactly the same when using non-generated providers.
It looks like the provider get disposed too early when using .selectAsync
and then never returns.
I've tracked down the issue and will submit a fix shortly.