-
-
Notifications
You must be signed in to change notification settings - Fork 977
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ref.watch returns a value other than ref.read #3889
Comments
I'd need a complete example. As is, this doesn't make sense to me |
Hm.. If I start monitoring the value by setting the listener in the |
My guess is more that your provider got disposed after |
That's the point that both ref.read and ref.watch are called synchronously, and ref.red returns the correct value and ref.watch returns the old value (that was before the notifier action that had been invoked before ref.read and ref.watch). Tricky behavior.
Yes, I understand, unfortunately I can't realize how to simplify the logic to fit the problem into a simple example. Was hoping for any hints.. Anyway, thanks for the quick response. |
Ok, it seems watching is not working properly if the side effect is called before watching. I'm still not sure whether it is a bug or some unevident feature. Flutter exampleimport 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
@riverpod
class My extends _$My
{
@override
Future<int> build() async
{
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _value,
);
return value;
}
Future<void> inc() async
{
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => _value = value + 1,
);
ref.invalidateSelf();
await future;
}
static int _value = 0;
}
void main()
{
runApp(const ProviderScope(
observers: [ DebugProviderObserver() ],
child: MyApp(),
));
}
class MyApp extends StatelessWidget
{
const MyApp({ super.key });
@override
Widget build(final BuildContext context)
{
return MaterialApp(
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends ConsumerStatefulWidget
{
const MyWidget({ super.key });
@override
ConsumerState<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends ConsumerState<MyWidget>
{
final deferredValue = Future.delayed(
const Duration(milliseconds: 140),
() => 'initialized',
);
@override
void initState()
{
super.initState();
// Calling the side effect before watching starts.
_change();
}
@override
Widget build(final BuildContext context)
{
return FutureBuilder(
initialData: 'initializing...',
future: deferredValue,
builder: (context, snapshot) => Center(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(snapshot.data!),
if (snapshot.connectionState == ConnectionState.done) ...[
_buildProvider(),
ElevatedButton(
onPressed: _changing ? null : _change,
child: const Text('Change'),
),
],
],
)),
);
}
Widget _buildProvider()
{
final value = ref.watch(myProvider).valueOrNull;
print('[ui] Showing $value');
return Text('value: $value');
}
Future<void> _change() async
{
setState(() => _changing = true);
try {
print('[ui] Changing');
await ref.read(myProvider.notifier).inc();
} finally {
setState(() => _changing = false);
}
}
var _changing = false;
}
class DebugProviderObserver extends ProviderObserver
{
const DebugProviderObserver();
@override
void didAddProvider(
final ProviderBase<Object?> provider,
final Object? value,
final ProviderContainer container,
)
{
logMessage('Provider ${provider.name} was initialized with $value');
}
@override
void didDisposeProvider(
final ProviderBase<Object?> provider,
final ProviderContainer container,
)
{
logMessage('Provider ${provider.name} was disposed');
}
@override
void didUpdateProvider(
final ProviderBase<Object?> provider,
final Object? previousValue,
final Object? newValue,
final ProviderContainer container,
)
{
logMessage(
'Provider ${provider.name} updated from $previousValue to $newValue'
);
}
@override
void providerDidFail(
final ProviderBase<Object?> provider,
final Object error,
final StackTrace stackTrace,
final ProviderContainer container,
)
{
logMessage('Provider ${provider.name} threw $error at $stackTrace');
}
void logMessage(final String message)
{
print('[riverpod] $message');
}
} Log flutter: [ui] Changing
flutter: [riverpod] Provider myProvider was initialized with AsyncLoading<int>()
flutter: [riverpod] Provider myProvider was disposed
flutter: [riverpod] Provider myProvider was initialized with AsyncLoading<int>()
flutter: [ui] Showing null
flutter: [riverpod] Provider myProvider updated from AsyncLoading<int>() to AsyncData<int>(value: 0)
flutter: [ui] Showing 0
flutter: [riverpod] Provider myProvider updated from AsyncLoading<int>() to AsyncData<int>(value: 1)
flutter: [riverpod] Provider myProvider was disposed
flutter: [riverpod] Provider myProvider was initialized with AsyncLoading<int>()
flutter: [ui] Showing null
flutter: [riverpod] Provider myProvider was disposed |
What's wrong here exactly? Your provider wasn't listener so it got disposed. |
As I understand it, |
If I remove the side effect from @override
void initState()
{
super.initState();
// _change();
} So, the problem is |
You don't watch the provider until 140ms elapse. So it can get disposed before then. Your UI did update, as we see various logs with different values |
Absolutely right. But after this delay shouldn't the |
It does. Hence why there's no dispose between the second The second dispose and reset to |
Right, but |
Listening the provider works correctly when started after calling the provider's side effect, so there is no problem in |
Hi @rrousselGit, what can you say? When watching starts during the side effect (before updating the provider's state), something breaks, two provider states are created there. Eventually, the provider's watching element becomes disposed, and |
I can run your example. I just need a bit of time to investigate the matter in depth. For now I'm on other features |
I am not sure that I understood your issue correctly, but this is how I modified your notifier and it seems to me that it now does what you want it to do. The @riverpod
class My extends _$My {
@override
Future<int> build() async {
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _initialValue,
);
return value;
}
Future<void> inc() async {
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => state = AsyncData(value + 1),
);
}
static final int _initialValue = 0;
} The value in the UI now starts as null, goes to 0 after the initial build of myProvider and then stays at 0. When tapping the change button the value is incremented and correctly shown in the UI. Is this how you expected this to work? |
Hi @RepliedSage11 and thanks for paying attention to the problem. The So, thus you've just broken the example that shows the existence of internal bug in the Anyway, thank you for trying to understand the problem. |
Sorry I wasn't able to help. I think what you are try to do is not supported by Riverpod and is mentioned in the docs. So I guess this is "expected behaviour" in a way.
class WidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
// Bad: the provider should initialize itself
ref.read(provider).init();
}
} FWIW I think there are better and less brittle ways to do what you are trying to do, like the example I mentioned in my original comment. |
The code you metioned is about initializing the provider's state. It says use Side effects are ment to happen at any time, that's a real life of real projects. The state of the app should work properly even when nothing in UI is watching the state at the time. Just imagine one widget is watching the provider, then this widget go away from the tree for a while (it can happen at any time during the user interaction, this is unpredictable). Now no widget is watching the provider, its state is disposing. Concurrently side effects can happen at any time. Then the widget returns to the tree and starts watching the provider again, and this widget is ment to rebuild due to the watching, but it doesn't because another state of the provider arises - that's the problem, and this code example demonstrates the problem. There is something wrong in the |
Here is the example demonstrating that Dart exampleimport 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:uuid/v4.dart';
part 'main_riverpod.g.dart';
@riverpod
class My extends _$My
{
@override
Future<int> build() async
{
final id = const UuidV4().generate();
print('[provider] <$id> building');
ref.onDispose(() {
print('[provider] <$id> disposing');
});
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _value,
);
return value;
}
Future<void> inc() async
{
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => _value = value + 1,
);
ref.invalidateSelf();
final newValue = await future;
print('[provider] changed to $newValue');
}
static int _value = 0;
}
Future<void> main() async
{
final container = ProviderContainer();
print('[ui] inc...');
await container.read(myProvider.notifier).inc();
print('[ui] inc done');
int? value;
print('[ui] listening...');
container.listen(myProvider, (prev, next) {
print('[ui] $prev -> $next');
value = next.valueOrNull;
});
Future.delayed(
const Duration(seconds: 3),
() => container.read(myProvider.notifier).inc(),
);
print('[ui] listening started');
while (value == null || value! < 2) {
await Future.delayed(const Duration(seconds: 1));
print('[ui] value is $value');
}
print('[ui] done');
} Log [ui] inc...
[provider] <3756fbf7-423d-49d1-98cd-d77592fcd321> building
[provider] <3756fbf7-423d-49d1-98cd-d77592fcd321> disposing
[provider] <4b2f0c96-0926-47e1-86f8-a61d06b8fe00> building
[provider] changed to 1
[ui] inc done
[ui] listening...
[provider] <5be5af19-6e84-4414-8628-9b488208fe80> building
[ui] listening started
[ui] AsyncLoading<int>() -> AsyncData<int>(value: 1)
[ui] value is 1
[ui] value is 1
[ui] value is 1
[provider] <5be5af19-6e84-4414-8628-9b488208fe80> disposing
[provider] <032414cd-0328-4968-bbba-b8659b889168> building
[ui] AsyncData<int>(value: 1) -> AsyncData<int>(isLoading: true, value: 1)
[ui] AsyncData<int>(isLoading: true, value: 1) -> AsyncData<int>(value: 2)
[provider] changed to 2
[ui] value is 2
[ui] done
Exited. |
Hello there @darkstarx! I put your code down below, trying to reproduce your example. Full repro, no tests (yet!)import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger_settings.dart';
final myProvider = AsyncNotifierProvider<My, int>(My.new);
class My extends AsyncNotifier<int> {
@override
FutureOr<int> build() async {
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _value,
);
return value;
}
Future<void> inc() async {
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => _value = value + 1,
);
ref.invalidateSelf();
await future;
}
static int _value = 0;
}
void main() {
runApp(
ProviderScope(
observers: [
TalkerRiverpodObserver(
settings: TalkerRiverpodLoggerSettings(
printProviderDisposed: true,
printFailFullData: true,
printProviderAdded: true,
printProviderFailed: true,
printProviderUpdated: true,
printStateFullData: true,
),
)
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(final BuildContext context) {
return MaterialApp(
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends ConsumerStatefulWidget {
const MyWidget({super.key});
@override
ConsumerState<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends ConsumerState<MyWidget> {
final deferredValue = Future.delayed(
const Duration(milliseconds: 140),
() => 'initialized',
);
@override
void initState() {
super.initState();
_change();
}
@override
Widget build(final BuildContext context) {
return FutureBuilder(
initialData: 'initializing...',
future: deferredValue,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(snapshot.data!),
if (snapshot.connectionState == ConnectionState.done) ...[
_buildProvider(),
ElevatedButton(
onPressed: _changing ? null : _change,
child: const Text('Change'),
),
],
],
),
),
);
}
Widget _buildProvider() {
final value = ref.watch(myProvider).valueOrNull;
print('[ui] Showing $value');
return Text('value: $value');
}
Future<void> _change() async {
setState(() => _changing = true);
try {
print('[ui] Changing');
await ref.read(myProvider.notifier).inc();
} finally {
setState(() => _changing = false);
}
}
var _changing = false;
} Please note Let's address your questions!
Yes.
Nah, it does (see down below).
Nah, this is a common misconception. Check my output. Output
All I see is expected behavior. Triggering the side effect will print the following: Output
Again, that's expected behavior. The changes triggers, you set ephimeral state to signal such action, thus another "showing 1" occurs. Then, the provider disposes and updates, and the logs follow accordingly.
Of course. Our responsibility as devs, tho, is put these "streams of uncontrolled events" into something we can control. Riverpod does this with uni-directional data flow and reactive programming.
I'd rather make the opposite statement: your UI should work properly and independently of incoming data. |
To summarize, the core issue is the following, right?
You might have hit a edge case in Flutter's peculiar algorithm. In summary, if Flutter evaluates that a Widget is still clean - its element won't disappear in the tree. This is clearly shown in my logs above. I've essentially hit a race condition in which a provider is being initialized via a AFAIK this is not a |
Hello @lucavenir and thanks for joining the discussion. In your example the provider is not auto-disposable, so it works fine. But the issue specifically concerns an auto-disposable one. Please, change the provider to this one and try again, and you'll see the problem. final myProvider = AutoDisposeAsyncNotifierProvider<My, int>(My.new);
class My extends AutoDisposeAsyncNotifier<int> {
@override
FutureOr<int> build() async {
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _value,
);
return value;
}
Future<void> inc() async {
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => _value = value + 1,
);
ref.invalidateSelf();
await future;
}
static int _value = 0;
} |
Woops. Sorry bout that. I'm way too accustomed to Also don't mind me wrongly assuming it was about a race condition 😜 (it happened to me in the past, it's just personal bias I guess). The actual example is the following. Full exampleimport 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger_settings.dart';
final myProvider = AutoDisposeAsyncNotifierProvider<My, int>(My.new);
class My extends AutoDisposeAsyncNotifier<int> {
@override
FutureOr<int> build() async {
final value = await Future.delayed(
const Duration(milliseconds: 100),
() => _value,
);
return value;
}
Future<void> inc() async {
final value = await future;
await Future.delayed(
const Duration(milliseconds: 280),
() => _value = value + 1,
);
ref.invalidateSelf();
await future;
}
static int _value = 0;
}
void main() {
runApp(
ProviderScope(
observers: [
TalkerRiverpodObserver(
settings: TalkerRiverpodLoggerSettings(
printProviderDisposed: true,
printFailFullData: true,
printProviderAdded: true,
printProviderFailed: true,
printProviderUpdated: true,
printStateFullData: true,
),
)
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(final BuildContext context) {
return MaterialApp(
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends ConsumerStatefulWidget {
const MyWidget({super.key});
@override
ConsumerState<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends ConsumerState<MyWidget> {
final deferredValue = Future.delayed(
const Duration(milliseconds: 140),
() => 'initialized',
);
@override
void initState() {
super.initState();
_change();
}
@override
Widget build(final BuildContext context) {
return FutureBuilder(
initialData: 'initializing...',
future: deferredValue,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(snapshot.data!),
if (snapshot.connectionState == ConnectionState.done) ...[
_buildProvider(),
ElevatedButton(
onPressed: _changing ? null : _change,
child: const Text('Change'),
),
],
],
),
),
);
}
Widget _buildProvider() {
final value = ref.watch(myProvider).valueOrNull;
print('[ui] Showing $value');
return Text('value: $value');
}
Future<void> _change() async {
setState(() => _changing = true);
try {
print('[ui] Changing');
await ref.read(myProvider.notifier).inc();
} finally {
setState(() => _changing = false);
}
}
var _changing = false;
} And the actual output (without interaction) is the following: Details
The problem as you can see is that adding your reactive dependency ( With "like that" I mean "in a function of your consumer stateful widget". Indeed if you write a I need to figure out if this is intended behavior or not, I'll get back here when I do. |
Yes, this is what I wrote about earlier, and that's why I told the problem seems to be in the |
Oh boy this was a hard one.
I'm here just to replicate bugs; then, if I'm able to replicate, summarize them, and write a self-containing failing test for 'em. Summarizing this issue to help @rrousselGit narrow down bug squashing before the 3.0 release. Self-containing failing testimport 'dart:async';
import 'package:riverpod/riverpod.dart';
// import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
void main() {
test("artifically waiting a side effect is allowed", () async {
final container = ProviderContainer(
// observers: [TalkerRiverpodObserver()], // add this for further debug info
);
addTearDown(container.dispose);
final future = container.read(fancyNotifierProvider.notifier).inc();
expectLater(future, completion(equals(1)));
expect(container.exists(fancyNotifierProvider), isTrue);
// "artificially" wait for side effect's future to complete
await Future.delayed(FancyNotifier._incDuration);
expect(container.exists(fancyNotifierProvider), isFalse);
final tester = container.testTransitionsOn(fancyNotifierProvider);
expect(container.exists(fancyNotifierProvider), isTrue);
final value = container.read(fancyNotifierProvider.future);
expectLater(value, completion(equals(1)));
// wait for the notifier's future to complete
await value;
// try adding the following with an observer on, and see what happens to your logs:
// await container.read(fancyNotifierProvider.future);
// await container.read(fancyNotifierProvider.future);
// await container.read(fancyNotifierProvider.future);
verifyInOrder([
() => tester(null, AsyncLoading()),
() => tester(AsyncLoading(), AsyncData(1)), // this doesn't happen at all!
]);
verifyNoMoreInteractions(tester);
});
}
final fancyNotifierProvider =
AutoDisposeAsyncNotifierProvider<FancyNotifier, int>(FancyNotifier.new);
class FancyNotifier extends AutoDisposeAsyncNotifier<int> {
@override
FutureOr<int> build() {
return Future.delayed(
_buildDuration,
() => _value,
);
}
Future<int> inc() async {
var value = await future;
await Future.delayed(
_incDuration,
() => _value = value + 1,
);
ref.invalidateSelf();
value = await future;
return value;
}
static int _value = 0;
static const Duration _incDuration = Duration(milliseconds: 200);
static const Duration _buildDuration = Duration(milliseconds: 100);
}
class ProviderTransitionsTestHelper<T> extends Mock {
void call(T? previous, T value);
}
extension ProviderTransitionListenerSetupExt on ProviderContainer {
ProviderTransitionsTestHelper<T> testTransitionsOn<T>(
ProviderListenable<T> provider, {
bool fireImmediately = true,
}) {
final listener = ProviderTransitionsTestHelper<T>();
final subscription = listen(
provider,
listener.call,
fireImmediately: fireImmediately,
);
addTearDown(subscription.close);
return listener;
}
} What happens here is that a To help visualize this behavior, I've left a handy logger for you to use, you can de-comment those lines if you want to see that as well. Below, the output, as-is. Logs
You can see the two "add" events. Adding (de-commenting) an extra The key points are:
To double and triple check this, I've wrote two more passing tests. First passing testimport 'dart:async';
import 'package:riverpod/riverpod.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
void main() {
test('waiting for the side effect to finish its computation', () async {
final container = ProviderContainer(
observers: [TalkerRiverpodObserver()], // add this for further debug info
);
addTearDown(container.dispose);
final future = container.read(fancyNotifierProvider.notifier).inc();
expectLater(future, completion(equals(1)));
expect(container.exists(fancyNotifierProvider), isTrue);
// "artificially" wait for side effect's future to complete
await future;
expect(container.exists(fancyNotifierProvider), isFalse);
final tester = container.testTransitionsOn(fancyNotifierProvider);
expect(container.exists(fancyNotifierProvider), isTrue);
final value = container.read(fancyNotifierProvider.future);
expectLater(value, completes);
// wait for the notifier's future to complete
await value;
verifyInOrder([
() => tester(null, AsyncLoading()),
() => tester(AsyncLoading(), AsyncData(1)),
]);
verifyNoMoreInteractions(tester);
});
}
final fancyNotifierProvider =
AutoDisposeAsyncNotifierProvider<FancyNotifier, int>(FancyNotifier.new);
class FancyNotifier extends AutoDisposeAsyncNotifier<int> {
@override
FutureOr<int> build() {
return Future.delayed(
_buildDuration,
() => _value,
);
}
Future<int> inc() async {
var value = await future;
await Future.delayed(
_incDuration,
() => _value = value + 1,
);
ref.invalidateSelf();
value = await future;
return value;
}
static int _value = 0;
static const Duration _incDuration = Duration(milliseconds: 200);
static const Duration _buildDuration = Duration(milliseconds: 100);
}
class ProviderTransitionsTestHelper<T> extends Mock {
void call(T? previous, T value);
}
extension ProviderTransitionListenerSetupExt on ProviderContainer {
ProviderTransitionsTestHelper<T> testTransitionsOn<T>(
ProviderListenable<T> provider, {
bool fireImmediately = true,
}) {
final listener = ProviderTransitionsTestHelper<T>();
final subscription = listen(
provider,
listener.call,
fireImmediately: fireImmediately,
);
addTearDown(subscription.close);
return listener;
}
} As you can see this test takes the same notifier, awaits for the side effect's Second passing testimport 'dart:async';
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
// import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart';
import 'helpers.dart';
void main() {
test("double-like state shouldn't be a thing", () async {
final container = ProviderContainer(
// observers: [TalkerRiverpodObserver()], // add this for further debug info
);
addTearDown(container.dispose);
final future = container.read(standardNotifierProvider.notifier).inc();
expectLater(future, completion(equals(1)));
// "artificially" wait for side effect's future to complete
await Future.delayed(StandardNotifier._incDuration);
final tester = container.testTransitionsOn(standardNotifierProvider);
final value = container.read(standardNotifierProvider.future);
expectLater(value, completion(equals(0)));
// wait for the notifier's future to complete
await value;
verifyInOrder([
() => tester(null, AsyncLoading()),
() => tester(AsyncLoading(), AsyncData(0)),
]);
verifyNoMoreInteractions(tester);
});
}
final standardNotifierProvider =
AutoDisposeAsyncNotifierProvider<StandardNotifier, int>(
StandardNotifier.new,
);
class StandardNotifier extends AutoDisposeAsyncNotifier<int> {
@override
FutureOr<int> build() {
return Future.delayed(
_buildDuration,
() => 0,
);
}
Future<int> inc() {
return update((state) {
return Future.delayed(_incDuration, () => state + 1);
});
}
static const Duration _incDuration = Duration(milliseconds: 200);
static const Duration _buildDuration = Duration(milliseconds: 100);
} The second passing test here uses |
@darkstarx I want to add this comment: one way to quickly solve this, is not to use an internal mutable static variable to handle state. |
I've gone deep into this bug and experimented some more, I've edited the summary above. |
Sure, that's absolutely correct behavior. Note that the variable in the example is static and won't be reset after disposing the notifier, it's there just to simulate storing the data outside (on some server) - just for simplification the example. |
Full repro here.
Describe the bug
I'm not sure, but it looks like a bug, and I wonder to know when it can happen, just to understand how to approach this problem.
To Reproduce
Expected behavior
The values are the same.
The text was updated successfully, but these errors were encountered: