riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

`widget cannot be marked as needing to build` when using an async provider and a menu

Open ValentinVignal opened this issue 4 months ago • 0 comments

Might be a duplicate of https://github.com/rrousselGit/riverpod/issues/3261

Describe the bug

When using a combination of async providers and a PopupMenuButton that uses a ProviderScope(parent: ProviderScope.containerOf(context)), there is an error widget cannot be marked as needing to build:

Logs
Launching lib/main.dart on macOS in debug mode...
Building macOS application...                                           
2024-02-23 17:11:03.637 flutter_app_stable[31808:326959] WARNING: Secure coding is automatically enabled for restorable state! However, not on all supported macOS versions of this application. Opt-in to secure coding explicitly by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState:.
Syncing files to device macOS...                                    50ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

A Dart VM Service on macOS is available at: http://127.0.0.1:59529/VMNAOAj5k4U=/
The Flutter DevTools debugger and profiler on macOS is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:59529/VMNAOAj5k4U=/
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: setState() or markNeedsBuild() called during build.
This _Popup widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  _Popup
The widget which was currently being built when the offending call was made was:
  UncontrolledProviderScope
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:5042:9)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:5054:6)
#2      ConsumerStatefulElement.watch.<anonymous closure>.<anonymous closure> (package:flutter_riverpod/src/consumer.dart:566:20)
#3      _RootZone.runBinaryGuarded (dart:async/zone.dart:1606:10)
#4      ProviderElementBase._notifyListeners.<anonymous closure> (package:riverpod/src/framework/element.dart:538:24)
#5      ResultData.map (package:riverpod/src/result.dart:74:16)
#6      ProviderElementBase._notifyListeners (package:riverpod/src/framework/element.dart:535:14)
#7      ProviderElementBase._performBuild (package:riverpod/src/framework/element.dart:375:7)
#8      ProviderElementBase.flush (package:riverpod/src/framework/element.dart:326:7)
#9      ProviderScheduler._performRefresh (package:riverpod/src/framework/scheduler.dart:94:41)
#10     ProviderScheduler._task (package:riverpod/src/framework/scheduler.dart:82:5)
#11     ProviderScheduler.vsync.<anonymous closure>.invoke (package:riverpod/src/framework/scheduler.dart:41:15)
#12     _UncontrolledProviderScopeElement.build (package:flutter_riverpod/src/framework.dart:390:12)
#13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5480:15)
#14     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#15     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2904:19)
#16     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:989:21)
#17     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:448:5)
#18     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15)
#19     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1311:9)
#20     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5)
#21     _invoke (dart:ui/hooks.dart:312:13)
#22     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5)
#23     _drawFrame (dart:ui/hooks.dart:283:31)

To Reproduce

Run the code and click on the PopupMenuButton "Popup" at the center of the screen.

Code sample

You can also checkout https://github.com/ValentinVignal/flutter_app_stable/tree/riveprod/widget-cannot-be-marked-as-needing-to-build-with-popup

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

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

final selectionProvider = Provider.autoDispose<Set<int>>((ref) {
  return const {};
});

final optionsProvider = StreamProvider<List<int>>((ref) async* {
  await Future<void>.delayed(const Duration(seconds: 1));
  yield List.generate(100, (index) => index);
});

final orderedOptionsProvider = Provider.autoDispose<AsyncValue<List<int>>>(
  (ref) {
    final selected = ref.watch(selectionProvider);
    return ref.watch(optionsProvider).whenData((options) {
      final selectedOptions = options.where((option) {
        return selected.contains(option);
      });
      final unselectedOptions = options.where((option) {
        return !selected.contains(option);
      });
      return selectedOptions.followedBy(unselectedOptions).toList();
    });
  },
  dependencies: [selectionProvider, optionsProvider],
);

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      home: ProviderScope(
        overrides: [
          selectionProvider.overrideWithValue(const {1, 2}),
        ],
        child: const Scaffold(
          body: _Content(),
        ),
      ),
    );
  }
}

class _Content extends ConsumerWidget {
  const _Content({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final container = ProviderScope.containerOf(context);
    return Center(
      child: PopupMenuButton(
        itemBuilder: (context) {
          return [
            PopupMenuChild(
              child: ProviderScope(
                parent: container,
                child: const _Popup(),
              ),
            ),
          ];
        },
        child: const Text('Popup'),
      ),
    );
  }
}

class _Popup extends ConsumerWidget {
  const _Popup({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final orderedOptions =
        ref.watch(orderedOptionsProvider).valueOrNull ?? const []; // <- No issue if I use `optionsProvider` instead
    final selection = ref.watch(selectionProvider);
    return Column(
      children: orderedOptions
          .map(
            (option) => CheckboxListTile(
              value: selection.contains(option),
              onChanged: (_) {},
              title: Text(option.toString()),
            ),
          )
          .toList(),
    );
  }
}

Expected behavior

I would expect not seeing any error thrown.


If it is any help for you, I don't face the error if the popup menu uses optionsProvider instead of orderedOptionsProvider

ValentinVignal avatar Feb 23 '24 09:02 ValentinVignal