riverpod
riverpod copied to clipboard
onCancel get unexpectedly called when the listener uses .select
Describe the bug
When a listener to a provider is using .select
and that listener rebuilds, ref.onCancel for the dependent provider is getting called.
To Reproduce
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
final fooProvider = Provider.autoDispose((ref) {
// Using .select here let onCancel being unexpectedly called
ref.watch(barProvider.select((v) => v));
});
final barProvider = Provider.autoDispose((ref) {
ref.onCancel(() {
print('onCancel called');
});
});
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(fooProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod example'),
),
body: Container(),
// Press the button to test the behavior
floatingActionButton: FloatingActionButton(
onPressed: () => ref.invalidate(fooProvider),
child: const Icon(Icons.add),
),
);
}
}
Expected behavior
I expect using .select
or not to act the same for ref.onCancel
behavior.
The following example might help spotting the issue. If the widget uses ref.listen (I guess it's related to .select in the previous sample), and that widget rebuilds, onCancel will get unexpectedly called. Using ref.watch instead of ref.listen work as expected.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
final barProvider = Provider.autoDispose((ref) {
ref.onCancel(() {
print('onCancel called');
});
});
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerStatefulWidget {
const MyHomePage({
Key? key
}) : super(key: key);
@override
ConsumerState<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends ConsumerState<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// Using ref.watch will not get onCancel called.
ref.listen(barProvider,(_,__){});
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Sounds more like a ref.watch bug when not using select
It calling onCancel makes sense, since the last listener was removed for a bit.
It calling onCancel makes sense, since the last listener was removed for a bit.
hmmm, I was expecting onCancel
not to be triggered if the last listener "widget/provider" just rebuilds.
This will totally invalidate the solution offered at https://github.com/rrousselGit/riverpod/issues/1329#issuecomment-1254137017 "combining ref.onCancel with ref.keepAlive()" as any rebuild to the widget will dispose the provider and break things.
The current behaviour is expected. So closing
@rrousselGit
Is it expected for onCancel
to be called with each provider rebuild?
This is something that can happen. Your code should be resilient to it.
I'll look for improvements on that area, but that's not a bug
Well, the different behavior of onCancel
when using/not using .select
is a bit confusing.
I hardly spotted that behavior change when I added .select
to some code in my project and it acted differently.
You can spot that by running the sample with ref.watch(barProvider.select((v) => v));
vs ref.watch(barProvider);
.
It happens with any modifiers, so with .future
& co too