Closed EthanHipps closed 1 year ago
Hi there,
there are no dumb questions.
ref.listen
shouldn't be called asynchronously
This is true. And it's also true for ref.watch
.
Why tho?
That's because registering a callback which reactively listens to other providers (i.e. events) might be dangerous if performed asynchronously. Imagine a scenario in which we await
for a "relatively long" time before registering such callback. By that time, that provider might have been disposed (runtime error) or - worse - it could have changed (silent bug).
This is why the docs state this shouldn't be done in a (e.g.) onPressed
async event or in other State
- lifecycles functions.
Luckily, when using ref.listenSelf
inside our own build
method this danger disappears; after the asynchronous gap, we might have the following scenarios:
AsyncError
AsyncNotifier
emits a loading
value, so nothing bad can really happenStateError
since you can't change state while initializing (I am sure you've received an error message like this before)Notifier
gets disposed. This won't happen as authentication is basically lisened at the root of our application. Nonetheless, it can be easily investigated if listenSelf
is safe with respect of this scenario with a simple reproducible example.Nonetheless, chances are I will edit that line out: obtaining SharedPreferences
is commonly done in a separated, synchronous Provider, so thanks for question. See, you can always help somebody.
Thank you very much for the response and detailed explanation. I really appreciate it!
I wanted to further explore this behavior. Here's a full reproducible example:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Riverpod demo here we go!'),
routes: {'new-route': (context) => const MyWidget()},
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text(
"Press the button to initialize the other screen and providers!")
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed('new-route');
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class MyWidget extends ConsumerWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final a = ref.watch(stateProvider).when(
data: (data) => "$data",
error: (error, stackTrace) => "Error",
loading: () => "loading...",
);
return Scaffold(
body: Center(
child: InkWell(
onTap: () async {
Navigator.of(context).pop();
await Future.delayed(const Duration(seconds: 3));
try {
// won't work (remove try catch to see what happens)
print("Reading 1... ${ref.read(otherProvider)}");
} catch (e) {}
try {
// won't work (remove try catch to see what happens)
print("Reading 2... ${ref.read(stateProvider)}");
} catch (e) {}
},
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(32),
child: Text(a),
),
),
),
);
}
}
@riverpod
Future<int> state(StateRef ref) async {
ref.onResume(() {
print("I am alive again!");
});
ref.onCancel(() {
print("I have no more listeners, I'm gonna be disposed!");
});
ref.onDispose(() {
print("I have been disposed!");
});
final result = await Future.delayed(
const Duration(seconds: 2),
() => Random().nextInt(10),
);
ref.listenSelf((previous, next) {
print("I have listened to myself: $next");
});
ref.listen(otherProvider, (previous, next) {
print("I have listened to other: $next");
});
ref.keepAlive();
return result;
}
@riverpod
Future<int> other(OtherRef ref) async {
ref.onResume(() {
print("[other] I am alive again!");
});
ref.onCancel(() {
print("[other] I have no more listeners, I'm gonna be disposed!");
});
ref.onDispose(() {
print("[other] I have been disposed!");
});
final result = await Future.delayed(
const Duration(seconds: 2),
() => Random().nextInt(10),
);
ref.listenSelf((previous, next) {
print("[other] I have listened to myself");
});
ref.keepAlive();
return result;
}
I invite you to test this out, and to go in and out the pushed route (press the button and then the rectangle) - and read the logs.
There's a few takeaways:
build
functions or inside providers, those instructions will be executed. So listen
and listenSelf
are executed anyways.ref.listen
won't crash anything, but will temporarily "awaken" other
and trigger the callback, which might be undesired. Then, the provider will be disposed (and its listeners removed). I might have said something incorrect about the state error above, but still, don't use listen
asynchronously.ref.listenSelf
isn't ever triggered in the first Provider, which is desired. So it's safe to use.
Hello,
I have a dumb question and apologize in advance if this isn't the correct place to ask it.
In auth.dart, the asynchronous
build()
calls_persistenceRefreshLogic()
which then callsref.listenSelf()
. According to the Riverpod docs,ref.listen()
shouldn't be called asynchronously.Does that guidance not specifically apply to
ref.listenSelf()
or this usage in general (i.e. if I wanted to callref.listen
in a similar method would it be okay)?