在构建期间调用 setState() 或 markNeedsBuild(),使用 FutureBuilder 中的 Provider 和 Flutter 中的 StreamBuilder

Posted

技术标签:

【中文标题】在构建期间调用 setState() 或 markNeedsBuild(),使用 FutureBuilder 中的 Provider 和 Flutter 中的 StreamBuilder【英文标题】:setState() or markNeedsBuild() called during build, using Provider inside FutureBuilder and StreamBuilder in Flutter 【发布时间】:2021-11-24 21:56:42 【问题描述】:

我有以下问题。当我的应用程序启动(用户登录)时,我需要从 firebase 读取值 accountId - 这是与用户帐户分开创建的帐户的 ID,该帐户存储在 Firestore 文档之一中。 在通过嵌套在main.dart 中的 FutureBuilder 获取accountId 之后,我将其保存通过

Provider.of<RegistrationHelper>(context.updateAccountId(accountId);

到我的班级RegistrationHelper 使其可用于其他地方。

问题是,虽然accountId 保存在RegistrationHelper 中,但我收到以下错误,您可以在底部找到。 这是我的main.dart 的代码。有谁知道如何解决这个问题? 非常感谢您的支持!

void main() async 
  WidgetsFlutterBinding.ensureInitialized();
  runApp(JustAnApp());


class EmotionsApp extends StatelessWidget 
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) 
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<RegistrationHelper>(
          create: (_) => RegistrationHelper(),
        ),
        ChangeNotifierProvider<EmotionsHelper>(
          create: (_) => EmotionsHelper(),
        ),
      ],
      child: MaterialApp(
        title: 'Jak się dziś czujesz?',
        theme: ThemeData(
          primarySwatch: Colors.purple,
          accentColor: Colors.orange,
          accentColorBrightness: Brightness.light,
          canvasColor: Color.fromRGBO(255, 254, 229, 1),
          backgroundColor: Colors.deepPurple,
          buttonTheme: ButtonTheme.of(context).copyWith(
            buttonColor: Colors.purple,
            textTheme: ButtonTextTheme.primary,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20),
            ),
          ),
          fontFamily: 'Raleway',
          textTheme: ThemeData.light().textTheme.copyWith(
                bodyText1: TextStyle(
                  color: Color.fromRGBO(20, 51, 51, 1),
                ),
                bodyText2: TextStyle(
                  color: Color.fromRGBO(20, 51, 51, 1),
                ),
                headline1: TextStyle(
                  fontSize: 20,
                  fontFamily: 'RobotoCondensed',
                  fontWeight: FontWeight.bold,
                ),
              ),
        ),
        home: FutureBuilder(
          future: _initialization,
          builder: (context, snapshot) 
            if (snapshot.hasError) 
              print('Snapshot error (main.dart) : $snapshot.error');
              return SomethingWentWrong();
            
            if (snapshot.connectionState == ConnectionState.waiting) 
              return Center(child: CircularProgressIndicator());
            

            if (snapshot.connectionState == ConnectionState.done) 
              // print('Snapshot (main.dart) : $snapshot.connectionState');
              return StreamBuilder(
                  stream: FirebaseAuth.instance.authStateChanges(),
                  builder: (context, streamSnapshot) 
                    if (streamSnapshot.data == null) return LoginScreen();

                    if (streamSnapshot.connectionState ==
                        ConnectionState.waiting)
                      return Center(
                        child: CircularProgressIndicator(),
                      );

                    if (streamSnapshot.hasData) 
                      //print('MAIN.DART streamSnapshot data: $streamSnapshot');
                      final user = FirebaseAuth.instance.currentUser;

                      return FutureBuilder(
//HERE IS WHERE I AM FETCHING ACCOUNT ID
                          future: FirebaseFirestore.instance
                              .collection('root')
                              .doc('users')
                              .collection('userData')
                              .doc(user!.uid)
                              .get(),
                          builder: (BuildContext context, AsyncSnapshot snap) 
                            if (snap.data == null)
                              return Center(child: CircularProgressIndicator());

                            if (snap.connectionState == ConnectionState.waiting)
                              return Center(
                                child: CircularProgressIndicator(),
                              );

                            if (snap.hasData) 
//AND HERE I AM STORING ACCOUNT ID TO REGISTRATION HELPER CLASS

                              Provider.of<RegistrationHelper>(context)
                                  .updateActualAccountId(
                                      snap.data['accountId']);

                              //return Text('accountId updated');
                            
                            return FacilitiesScreen();
                          );
                     else 
                      return LoginScreen();
                    
                  );
            
            return Center(child: CircularProgressIndicator());
          ,
        ),
        routes: 
          ...
        ,
      ),
    );
  

以及RegistrationHelper中的一个简单方法:

 void updateActualAccountId(String accountId) 
    actualAccountId = accountId;
    notifyListeners();
  

我得到的错误:

    ======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for RegistrationHelper:
setState() or markNeedsBuild() called during build.

This _InheritedProviderScope<RegistrationHelper> 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: _InheritedProviderScope<RegistrationHelper>
  value: Instance of 'RegistrationHelper'
  listening to value
The widget which was currently being built when the offending call was made was: FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>
  dirty
  state: _FutureBuilderState<DocumentSnapshot<Map<String, dynamic>>>#bdfb1
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4305:11)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4320:6)
#2      _InheritedProviderScopeElement.markNeedsNotifyDependents (package:provider/src/inherited_provider.dart:531:5)
#3      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:308:24)
#4      RegistrationHelper.updateActualAccountId (package:emotions4_flutter/auth/auth_registration_helper.dart:42:5)
#5      _FacilitiesScreenState._updateAccountId (package:emotions4_flutter/screens/facilities_screen.dart:27:10)
#6      _FacilitiesScreenState.build.<anonymous closure> (package:emotions4_flutter/screens/facilities_screen.dart:101:25)
#7      _FutureBuilderState.build (package:flutter/src/widgets/async.dart:782:55)
#8      StatefulElement.build (package:flutter/src/widgets/framework.dart:4782:27)
#9      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4665:15)
#10     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4840:11)
#11     Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5)
#12     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:33)
#13     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
#14     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319:5)
#15     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1143:15)
#16     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1080:9)
#17     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:996:5)
#21     _invoke (dart:ui/hooks.dart:166:10)
#22     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:270:5)
#23     _drawFrame (dart:ui/hooks.dart:129:31)
(elided 3 frames from dart:async)
The RegistrationHelper sending notification was: Instance of 'RegistrationHelper'
====================================================================================================

【问题讨论】:

【参考方案1】:

尝试使用有状态小部件:

class EmotionsApp extends StatefulWidget 
  @override
  State<EmotionsApp> createState() => _EmotionsAppState();


class _EmotionsAppState extends State<EmotionsApp> 
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) 
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<RegistrationHelper>(
          create: (_) => RegistrationHelper(),
        ),
        ChangeNotifierProvider<EmotionsHelper>(
          create: (_) => EmotionsHelper(),
        ),
      ],
      child: MaterialApp(
        title: 'Jak się dziś czujesz?',
        theme: ThemeData(
          primarySwatch: Colors.purple,
          accentColor: Colors.orange,
          accentColorBrightness: Brightness.light,
          canvasColor: Color.fromRGBO(255, 254, 229, 1),
          backgroundColor: Colors.deepPurple,
          buttonTheme: ButtonTheme.of(context).copyWith(
            buttonColor: Colors.purple,
            textTheme: ButtonTextTheme.primary,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20),
            ),
          ),
          fontFamily: 'Raleway',
          textTheme: ThemeData.light().textTheme.copyWith(
            bodyText1: TextStyle(
              color: Color.fromRGBO(20, 51, 51, 1),
            ),
            bodyText2: TextStyle(
              color: Color.fromRGBO(20, 51, 51, 1),
            ),
            headline1: TextStyle(
              fontSize: 20,
              fontFamily: 'RobotoCondensed',
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        home: FutureBuilder(
          future: _initialization,
          builder: (context, snapshot) 
            if (snapshot.hasError) 
              print('Snapshot error (main.dart) : $snapshot.error');
              return SomethingWentWrong();
            
            if (snapshot.connectionState == ConnectionState.waiting) 
              return Center(child: CircularProgressIndicator());
            

            if (snapshot.connectionState == ConnectionState.done) 
              // print('Snapshot (main.dart) : $snapshot.connectionState');
              return StreamBuilder(
                  stream: FirebaseAuth.instance.authStateChanges(),
                  builder: (context, streamSnapshot) 
                    if (streamSnapshot.data == null) return LoginScreen();

                    if (streamSnapshot.connectionState ==
                        ConnectionState.waiting)
                      return Center(
                        child: CircularProgressIndicator(),
                      );

                    if (streamSnapshot.hasData) 
                      //print('MAIN.DART streamSnapshot data: $streamSnapshot');
                      final user = FirebaseAuth.instance.currentUser;

                      return FutureBuilder(
//HERE IS WHERE I AM FETCHING ACCOUNT ID
                          future: FirebaseFirestore.instance
                              .collection('root')
                              .doc('users')
                              .collection('userData')
                              .doc(user!.uid)
                              .get(),
                          builder: (BuildContext context, AsyncSnapshot snap) 
                            if (snap.data == null)
                              return Center(child: CircularProgressIndicator());

                            if (snap.connectionState == ConnectionState.waiting)
                              return Center(
                                child: CircularProgressIndicator(),
                              );

                            if (snap.hasData) 
//AND HERE I AM STORING ACCOUNT ID TO REGISTRATION HELPER CLASS

                              Provider.of<RegistrationHelper>(context)
                                  .updateActualAccountId(
                                  snap.data['accountId']);

                              //return Text('accountId updated');
                            
                            return FacilitiesScreen();
                          );
                     else 
                      return LoginScreen();
                    
                  );
            
            return Center(child: CircularProgressIndicator());
          ,
        ),
        routes: 
          ...
        ,
      ),
    );
  

在小部件中实例化某些东西(此处为 _initialization)的问题在于,小部件将在其可见时被实例化多次(基本上每次调用其父级 build)。您想要的是在小部件位于屏幕上时仅创建一个 _initialization 对象。为此,您必须在 StatefulWidgetState 中创建它,因为 State 是长期存在的,因此只会被实例化一次。

【讨论】:

嗨,感谢您的意见@Lulupointu。我希望它就像将无状态小部件更改为有状态小部件一样简单。不幸的是,如果您只是想更改为有状态的小部件,它并没有改变任何东西。还是一样的setState() or markNeedsBuild() called during build.,它仍然指向方法:Provider.of(context).updateActualAccountId(snap.data['accountId']);在错误日志中。 即使在didChangeDependencies 中使用Provider.of&lt;RegistrationHelper&gt;(context).updateActualAccountId(snap.data['accountId']); ?如果是这样,如果您将其包装在 WidgetBindings.instance.addPostFramCallback 中会发生这种情况吗? 抱歉耽搁了,我出差了,没有机会坐下来玩电脑。当我将整个 FutureBuilder 包括 Provider.of.... 方法放入 didChangeDependencies (在状态部分)时,我得到了另一个错误:[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp() 不用担心。您的新错误完全不同,这是因为您忘记按照此处的说明初始化 firebase:***.com/questions/63492211/…【参考方案2】:

似乎可行的解决方案是从主屏幕文件(而不是 main.dart)中的“didChangeDependencies”调用 RegistrationHelper 方法,如下所示:

  @override
  void didChangeDependencies() 
    Provider.of<RegistrationHelper>(context, listen: false).getAccountId();
    super.didChangeDependencies();
  

没有错误了。非常感谢您的帮助!

【讨论】:

以上是关于在构建期间调用 setState() 或 markNeedsBuild(),使用 FutureBuilder 中的 Provider 和 Flutter 中的 StreamBuilder的主要内容,如果未能解决你的问题,请参考以下文章

在构建期间调用 setState() 或 markNeedsBuild() - Flutter

Flutter:在构建期间调用 setState() 或 markNeedsBuild()

在构建 CupertinoTabScaffold 期间调用 setState() 或 markNeedsBuild()

错误:在构建期间调用了 setState() 或 markNeedsBuild()

在构建异常期间调用的 setState() 或 markNeedsBuild() 阻止我执行回调

Flutter:在构建错误期间调用了 setState() 或 markNeedsBuild()