riverpod
riverpod copied to clipboard
ref.read() gets previous state, instead of the same value as next from ref.listen()
Describe the bug I am listening for a state change, and when it occurs triggering some separate logic to do some more complex checks on the state. However, ref.listen() is updated, but when triggering the other code, the ref.read() gets the previous value.
It appears to only happen for AsyncValue.
To Reproduce
//example_provider.dart
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'example_provider.g.dart';
@riverpod
class Example extends _$Example {
int counter = 0;
@override
Future<String> build() async {
return "$counter";
}
void increment() {
debugPrint("Updated counter from $counter to ${++counter}");
state = AsyncData("$counter");
}
}
//watcher_provider.dart
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'example_provider.dart';
part 'watcher_provider.g.dart';
@riverpod
void eventWatcher(EventWatcherRef ref) {
ref.listen(exampleProvider, (previous, next) async {
final readValue = await ref.read(exampleProvider.future);
debugPrint(
"Previous: ${previous?.valueOrNull}, next: ${next.valueOrNull}, read: $readValue");
});
}
//main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mre_riverpod_outdated_read/example_provider.dart';
import 'watcher_provider.dart';
void main() {
runApp(
const ProviderScope(
child: RiverpodEagerInit(
child: MyApp(),
),
),
);
}
class RiverpodEagerInit extends ConsumerWidget {
const RiverpodEagerInit({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(eventWatcherProvider);
return child;
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(exampleProvider).maybeWhen(
data: (data) => data,
orElse: () => "-1",
);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
counter,
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: ref.read(exampleProvider.notifier).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
From debugging:
flutter: Previous: null, next: 0, read: 0
flutter: Updated counter from 0 to 1 // button pressed
flutter: Previous: 0, next: 1, read: 0 // bug occurs, expected read to get "1"
Expected behavior ref.read() gets the same value as ref.listen() next
Could you share something I can run?
I have updated the code, and pasted in a reproducible example
I don't know the inner workings of riverpod that well, but debugging through the code I have found this:
https://github.com/rrousselGit/riverpod/blob/3d3e57f070d16addf9f35af4bc57d7ae65da90a8/packages/riverpod/lib/src/async_notifier/base.dart#L282-L294
The asyncTransition method is what actually updates the state and calls the listeners of .listen and .watch clients. It is called before the future property is reassigned, so in your watcher you are still awaiting the previous future.
ref.listen(exampleProvider, (previous, next) async {
// called when asyncTransition already happened and state is already 'next'
final readValue = await ref.read(exampleProvider.future); // <-- but not before the future was reassigned to the new value
debugPrint(
"Previous: ${previous?.valueOrNull}, next: ${next.valueOrNull}, read: $readValue");
});
Maybe this needs to be mentioned in the docs. (Or is this not intended behaviour? @rrousselGit)
Hi @JifScharp and hi @RepliedSage11,
I'm not the maintainer but I would say this is not the "intended usage" of ref.listen.
Doing...
ref.listen(someProvider, (previous, next) async {
final lol = await ref.read(someProvider.future);
// do whatever with "lol"
}
... definitely smells. Most likely this is an xy-problem.
The correct API usage would be:
ref.listen(someProvider.future, (previous, next) async {
final lol = await next;
// do whatever with "lol"
}
But I digress.
Writing down here a self-containing and failing test, so that @rrousselGit can verify which path to take.
repro w/ failing test
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';
final exampleProvider = AutoDisposeAsyncNotifierProvider<Example, int>(
() => Example(),
);
class Example extends AutoDisposeAsyncNotifier<int> {
@override
Future<int> build() async {
return 0;
}
Future<void> increment() async {
final prev = state;
await update((state) => state + 1);
debugPrint(
"Updated counter from ${prev.valueOrNull} to ${state.valueOrNull}",
);
}
}
void main() {
test('self-read inside a ref.listen is consistent', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
late int? prev;
late int? next;
late int read;
container.listen(exampleProvider, (p, n) async {
final readValue = await container.read(exampleProvider.future);
prev = p?.valueOrNull;
next = n.valueOrNull;
read = readValue;
debugPrint(
"Previous: ${p?.valueOrNull}, next: ${n.valueOrNull}, read: $readValue",
);
});
await container.read(exampleProvider.future);
await container.pump();
expect(prev, isNull);
expect(next, 0);
expect(read, 0);
container.read(exampleProvider.notifier).increment();
await container.read(exampleProvider.future);
await container.pump();
expect(prev, 0);
expect(next, 1);
expect(read, 1); // fails here
});
}
Tbh you're not really supposed to use ref inside listeners.
I've recently disabled using ref within onDispose and other life-cycle hooks. Not sure if I did it inside ref.listen too, but maybe I should.
I'm using ref inside listeners. I'm sure many are. We would want a good migration guide if that becomes prohibited.
What are you using it for?
The behavior of ref inside listeners/life-cycles is undetermined/untested. I wouldn't be able to tell you for sure what happens in some cases or whether there's no unexpected bug
Toggling another provider on/off when opening/closing this provider, (closed means it is toggled off, hidden means it is toggled on but not displayed because another provider became open (toggled on and displayed). It seems to work as expected. I had to do it via an outside listener because it would be a circular dependency if it was in the two providers (there is an equivalent listener listening to detailsShownProvider).
ref.listen(filtersShownProvider(_locality), (PanelState? previous, PanelState next) {
final PanelState detailsShown = ref.read(detailsShownProvider(_locality));
if (previous != next && next.isClosed && detailsShown == PanelState.hidden) {
ref.read(detailsShownProvider(_locality).notifier).open();
} else if (previous != next && next.isOpen && detailsShown == PanelState.open) {
ref.read(detailsShownProvider(_locality).notifier).hide();
}
});
Sounds reasonable.
I've just realised that this is a duplicate of https://github.com/rrousselGit/riverpod/issues/4185
Toggling another provider on/off when opening/closing this provider, (closed means it is toggled off, hidden means it is toggled on but not displayed because another provider became open (toggled on and displayed). It seems to work as expected. I had to do it via an outside listener because it would be a circular dependency if it was in the two providers (there is an equivalent listener listening to
detailsShownProvider).ref.listen(filtersShownProvider(_locality), (PanelState? previous, PanelState next) { final PanelState detailsShown = ref.read(detailsShownProvider(_locality)); if (previous != next && next.isClosed && detailsShown == PanelState.hidden) { ref.read(detailsShownProvider(_locality).notifier).open(); } else if (previous != next && next.isOpen && detailsShown == PanelState.open) { ref.read(detailsShownProvider(_locality).notifier).hide(); } });
Hey @benji-farquhar,
You can do something similar to this:
enum PanelState { hidden, open, closed }
extension PanelStateX on PanelState {
bool get isOpen => this == PanelState.open;
bool get isClosed => this == PanelState.closed;
}
@riverpod
class DetailsShown extends _$DetailsShown {
@override
PanelState build(String locality) => PanelState.hidden;
void open() => state = PanelState.open;
void hide() => state = PanelState.hidden;
}
@riverpod
class FiltersShown extends _$FiltersShown {
@override
PanelState build(String locality) => PanelState.hidden;
void open() => state = PanelState.open;
void hide() => state = PanelState.hidden;
}
Then instead of ref.listen, use a sync provider:
@riverpod
void panelSync(PanelSyncRef ref, String locality) {
final filters = ref.watch(filtersShownProvider(locality));
final details = ref.watch(detailsShownProvider(locality));
final detailsNotifier = ref.read(detailsShownProvider(locality).notifier);
if (filters.isClosed && details == PanelState.hidden) {
detailsNotifier.open();
} else if (filters.isOpen && details == PanelState.open) {
detailsNotifier.hide();
}
}
And in your widget:
ref.watch(panelSyncProvider(locality));