Skip to content
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

Open
darkstarx opened this issue Dec 23, 2024 · 13 comments
Open

ref.watch returns a value other than ref.read #3889

darkstarx opened this issue Dec 23, 2024 · 13 comments
Assignees
Labels
bug Something isn't working question Further information is requested

Comments

@darkstarx
Copy link

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

    final readPricePlan = ref.read(myPricePlanProvider).valueOrNull;
    final myPricePlan = ref.watch(myPricePlanProvider).valueOrNull;
    if (readPricePlan != myPricePlan) {
      print('!!!=== Oooops!');
    }

Expected behavior
The values are the same.

@darkstarx darkstarx added bug Something isn't working needs triage labels Dec 23, 2024
@rrousselGit
Copy link
Owner

I'd need a complete example. As is, this doesn't make sense to me

@rrousselGit rrousselGit added question Further information is requested and removed needs triage labels Dec 23, 2024
@darkstarx
Copy link
Author

Hm.. If I start monitoring the value by setting the listener in the initState (ref.listenManual), ref.watch starts returning correct value in my ConsumerState. Looks like something wrong with dependencies in the WidgetRef.watch.

@rrousselGit
Copy link
Owner

My guess is more that your provider got disposed after read, and therefore watch created a new value

@darkstarx
Copy link
Author

My guess is more that your provider got disposed after read, and therefore watch created a new value

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.

I'd need a complete example. As is, this doesn't make sense to me

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, I'll try to write an example with reproducible problem.

@darkstarx
Copy link
Author

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 example
import '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

@rrousselGit
Copy link
Owner

What's wrong here exactly? Your provider wasn't listener so it got disposed.

@darkstarx
Copy link
Author

darkstarx commented Dec 24, 2024

As I understand it, ref.watch is supposed to rebuild the widget every time the watched provider changes. But the widget doesn't rebuild when the provider changes in this example. Also I suppose the ref.watch adds a dependency which prevents the disposing of the provider, but it disposes.

@darkstarx
Copy link
Author

darkstarx commented Dec 24, 2024

If I remove the side effect from initState, so ref.watch is called earlier than this side effect, the widget starts being rebuilt every time the provider changes.

  @override
  void initState()
  {
    super.initState();
    // _change();
  }

So, the problem is ref.watch doesn't work properly if ref.read(provider.notifier).makeSideEffect() is called earlier than starting watching the provider.

@rrousselGit
Copy link
Owner

You don't watch the provider until 140ms elapse. So it can get disposed before then.
And your Inc method calls ref.invalidateSelf, which also causes the provider to be disposed.

Your UI did update, as we see various logs with different values

@darkstarx
Copy link
Author

You don't watch the provider until 140ms elapse. So it can get disposed before then.

Absolutely right. But after this delay shouldn't the ref.watch start watching the provider making it alive all the time the widget exists in the widget tree?

@rrousselGit
Copy link
Owner

It does. Hence why there's no dispose between the second AsyncLoading() and aAsyncData(1)

The second dispose and reset to null is because you called invalidateSelf

@darkstarx
Copy link
Author

Right, but ref.watch didn't go anywhere after that, and the widget is still waiting for new data from the provider after its rebuild. However, the widget does not display the new value (1), and only displays the intermediate loading state after invalidateSelf. It seems strange for me.

@darkstarx
Copy link
Author

Listening the provider works correctly when started after calling the provider's side effect, so there is no problem in riverpod. As I can see, there is some bug in consumer.dart of flutter_riverpod with dependencies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants