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

AutoDisposeProvider does not recreate value in nested ScopedProvider #1943

Closed
limenote135 opened this issue Nov 24, 2022 · 13 comments · Fixed by #2341
Closed

AutoDisposeProvider does not recreate value in nested ScopedProvider #1943

limenote135 opened this issue Nov 24, 2022 · 13 comments · Fixed by #2341
Labels
bug Something isn't working

Comments

@limenote135
Copy link

limenote135 commented Nov 24, 2022

Describe the bug

Exception 'Bad state: Trying to read an uninitialized value.' is thrown.

To Reproduce

With the following code, tap 'Toggle Button' more than three times.
When tapping three or five times, the value '0' is shown but "created!" log is not. And when tapping four or six times, "created!" and "disposed" are shown at the nearly same time.

After tapping three or six times, the exception is thrown if the button in the SampleWidget is clicked

I/flutter (20308): 2022-11-24 07:12:31.779970: created!
I/flutter (20308): 2022-11-24 07:12:34.825532: disposed!
I/flutter (20308): 2022-11-24 07:12:43.304902: created!
I/flutter (20308): 2022-11-24 07:12:43.306041: disposed!
I/flutter (20308): 2022-11-24 07:12:52.369469: created!
I/flutter (20308): 2022-11-24 07:12:52.369828: disposed!

Expected behavior

Update provider value.


class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("demo"),
      ),
      body: const _Body(),
    );
  }
}

final provider = StateProvider.autoDispose<int>((ref) {
  ref.onDispose(() {
    print("${DateTime.now()}: disposed!");
  });

  print("${DateTime.now()}: created!");
  return 0;
});

class _Body extends HookConsumerWidget {
  const _Body({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final showButton = useState(false);

    return ProviderScope(
      child: ProviderScope(
        child: ProviderScope(
          child: Center(
            child: Column(
              children: [
                ElevatedButton(
                    onPressed: () {
                      showButton.value = !showButton.value;
                    },
                    child: const Text("Toggle Button")),
                if (showButton.value) const SampleWidget(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class SampleWidget extends ConsumerWidget {
  const SampleWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(provider);

    return ElevatedButton(
      child: Text("$value"),
      onPressed: () {
        ref.read(provider.notifier).state = 1;
      },
    );
  }
}

Stacktrace:

======== Exception caught by gesture ===============================================================
The following StateError was thrown while handling a gesture:
Bad state: Trying to read an uninitialized value.

When the exception was thrown, this was the stack: 
#0      ProxyElementValueNotifier.value (package:riverpod/src/listenable.dart:25:7)
#1      ProviderElementProxy.read (package:riverpod/src/framework/proxy_provider_listenable.dart:117:28)
#2      ProviderContainer.read (package:riverpod/src/framework/container.dart:251:21)
#3      ConsumerStatefulElement.read (package:flutter_riverpod/src/consumer.dart:607:59)
#4      SampleWidget.build.<anonymous closure> (package:riverpod_sample/main.dart:86:13)
#5      _InkResponseState.handleTap (package:flutter/src/material/ink_well.dart:1072:21)
#6      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:253:24)
#7      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:627:11)
#8      BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:306:5)
#9      BaseTapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:239:7)
#10     PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:615:9)
#11     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
#12     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9)
#13     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:617:13)
#14     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
#15     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
#16     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:460:19)
#17     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:440:22)
#18     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:337:11)
#19     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:395:7)
#20     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:357:5)
#21     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:314:7)
#22     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:295:7)
#23     _invoke1 (dart:ui/hooks.dart:167:13)
#24     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:341:7)
#25     _dispatchPointerDataPacket (dart:ui/hooks.dart:94:31)
Handler: "onTap"
Recognizer: TapGestureRecognizer#f5580
  debugOwner: GestureDetector
  state: possible
  won arena
  finalPosition: Offset(198.9, 189.4)
  finalLocalPosition: Offset(34.5, 17.3)
  button: 1
  sent tap down
====================================================================================================
@limenote135 limenote135 added bug Something isn't working needs triage labels Nov 24, 2022
@rrousselGit
Copy link
Owner

And what's the issue / expected behavior?

@rrousselGit rrousselGit added question Further information is requested and removed needs triage labels Nov 24, 2022
@limenote135
Copy link
Author

limenote135 commented Nov 24, 2022

ah, I updated the code and description. The issue is the exception 'Bad state: Trying to read an uninitialized value.' is thrown.

@rrousselGit
Copy link
Owner

What's the stacktrace?

@limenote135
Copy link
Author

Added stacktrace.

@motrieux-thomas
Copy link

Same issue here, I added a temporary ref.keepAlive() in my StateNotifierProvider.
Not sure of this, but this problem seems to occurs more often on StateNotifierProviders.

@jeiea
Copy link
Contributor

jeiea commented Dec 6, 2022

I reproduced this with the following test.

   test('3-depth autoDispose', () async {
      final a = createContainer();
      final b = createContainer(parent: a);
      final c = createContainer(parent: b);

      var count = 0;
      final autoDisposeProvider = Provider.autoDispose((ref) {
        count += 1;
        ref.onDispose(() {
          count -= 1;
        });
        return 3;
      });

      var subscription = c.listen(
        autoDisposeProvider,
        (previous, next) {},
        fireImmediately: true,
      );
      await a.pump();
      expect(count, 1);

      subscription.close();
      await a.pump();
      expect(count, 0);

      subscription = c.listen(
        autoDisposeProvider,
        (previous, next) {},
        fireImmediately: true,
      );
      await a.pump();
      expect(count, 1);

      subscription.close();
      await a.pump();
      expect(count, 0);
    });

So my investigation is this.
In this test, Riverpod saves state readers in containers a (the root) and c. But when disposing, Riverpod searches for state readers in containers using a top-down approach and checks if the containers have same state readers. Because container b doesn't have the state reader, Riverpod stops searching and the state reader in container c remains alive and is not disposed.

I can make the test pass by commenting out the conditional, but I can't sure it is right way.

@rich-j
Copy link

rich-j commented Dec 13, 2022

Here's another example of this issue that is based on the DartPad Riverpod example:

Example code
// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// This is a reimplementation of the default Flutter application
// using riverpod.
void main() {
  runApp(
    // Adding ProviderScope enables Riverpod for the entire project
    const ProviderScope(child: MyApp()),
  );
}

/// Providers are declared globally and specify how to create a state
final counterProvider = StateProvider((ref) => 0);


// BUG - Auto disposed provider tracks the above counter
final derivedProvider = Provider.autoDispose((ref) =>
                                             ref.watch(counterProvider));

// BUG - Use the above provider
class DerivedCounter extends ConsumerWidget {
  @override Widget build(BuildContext context, WidgetRef ref) {
    final derivedCount = ref.watch(derivedProvider);
    return Text('$derivedCount <-- Should track counter!!');
  }
}


class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod example'),
      ),
      // BUG - At least 2 additional scopes cause the issue
      body: ProviderScope(
        child: ProviderScope(
        child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            // BUG - Usage of the provider is transient
            // This value should track the counter!!!
            if (count != 1) DerivedCounter(),
            Consumer(
              builder: (context, ref, _) {
                return Text(
                  '$count',
                  key: const Key('counterState'),
                  style: Theme.of(context).textTheme.headlineMedium,
                );
              },
            ),
          ],
        ),
      ))),
      floatingActionButton: FloatingActionButton(
        key: const Key('increment_floatingActionButton'),
        // The read method is a utility to read a provider without listening to it
        onPressed: () => ref.read(counterProvider.notifier).state++,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Copy this code into DartPad and run - clicking the button to increment the count should also update the derived count.

As reported above, the issue requires .autoDispose, nested ProviderScopes and happens in riverpod 2.1.1.

This issue was difficult for us to track down since not all of our code paths create the multiple nested scopes that cause the failure.

@josh-burton
Copy link
Contributor

I think I'm also running into something similar. Multiple levels of nested providerscopes, and some providers intermittently are not updating.

@SilverMira
Copy link

Pretty sure I'm facing the same issue too, a ref.watch nested in 2 ProviderScope doesn't trigger a widget rebuild when the state changes, though my scenario has more complexity with StateNotifierProvider deriving from a Provider that came from overrides. Huge blocker to my app currently due to how its structured, hope this gets fixed soon. Thought about opening an issue but looks like the example from rich-j's comment is pretty much spot on and saved me from drilling down a bunch of overrides finding what was the fault

@rrousselGit rrousselGit removed the question Further information is requested label Feb 14, 2023
@Praveena0989
Copy link

Praveena0989 commented Feb 20, 2023

I am also facing this issue. @rrousselGit were you able to reproduce the issue, or do you need help in reproducing?

@keithcwk
Copy link

keithcwk commented Feb 24, 2023

@rrousselGit Bumping this issue

Facing a similar issue to this as well

For my case scenario, it also involves nesting of ProviderScope objects. This is just a pseudocode structure of my use case. An API will return the NewsObject data in the form of a list, and a sliver list is generated, which each of the child being a ProviderScope object.

In each news card, there is a bookmark button, which will perform an API call to update the value of bookmark, and the ProviderScope is expected to rebuild, changing the UI of the BookmarkButton. In the previous version i've been using 2.0.0-dev.9, the ProviderScope and UI updates properly, however, I noticed that from version 2.0.0 onwards, this method does not work anymore.

ProviderScope(
      overrides: [
        newsCardStateProvider.overrideWithValue(
          NewsObject(bookmark: true),
        ),
      ],
      child: NewsCard(
        child: Column(
          children: [
            Text(news.title),
            ProviderScope(
              overrides: [
                bookmarkButtonStateProvider.overrideWithValue(
                  Bookmarked(
                    bookmark: newsObject.bookmark,
                  ),
                ),
              ],
              child: BookmarkButton(),
            ),
          ],
        ),
      ),
    );

@ykaito21
Copy link

ykaito21 commented Mar 8, 2023

my issue #2198 could be related to this?

@Praveena0989
Copy link

@ykaito21 Yes. It is the same issue

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

Successfully merging a pull request may close this issue.

10 participants