“在构建期间调用 setState() 或 markNeedsBuild()”错误尝试在消费者小部件(提供程序包)内的导航器中推送替换

Posted

技术标签:

【中文标题】“在构建期间调用 setState() 或 markNeedsBuild()”错误尝试在消费者小部件(提供程序包)内的导航器中推送替换【英文标题】:"setState() or markNeedsBuild() called during build" error trying to push a replacement in Navigator inside a Consumer widget (provider package) 【发布时间】:2020-01-21 15:07:24 【问题描述】:

这周我开始在 Flutter 中开发,我无法解决这个问题。

我正在构建一个登录页面,该页面调用 API 进行登录,然后重定向到主页。 这是Navigator.pushReplacement 在第一个代码块中生成的异常。 在那一刻,apiCall.isFetching 是错误的,因为提取已结束,apiCall.response 包含所需的数据。

异常详情:

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Consumer<ApiCallChangeNotifier>(dirty, dependencies: [InheritedProvider<ApiCallChangeNotifier>]):
setState() or markNeedsBuild() called during build.

This Overlay 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: Overlay-[LabeledGlobalKey<OverlayState>#4dc85]
  state: OverlayState#bd97e(tickers: tracking 1 ticker, entries: [OverlayEntry#2941b(opaque: false; maintainState: false), OverlayEntry#37814(opaque: false; maintainState: true), OverlayEntry#f92c0(opaque: false; maintainState: false), OverlayEntry#da26d(opaque: false; maintainState: true)])
The widget which was currently being built when the offending call was made was: Consumer<ApiCallChangeNotifier>
  dirty
  dependencies: [InheritedProvider<ApiCallChangeNotifier>]
User-created ancestor of the error-causing widget was: 
  Expanded file:///C:/flutter_test/lib/screens/login/LoginScreen.dart:153:37
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:3687:11)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:3702:6)
#2      State.setState (package:flutter/src/widgets/framework.dart:1161:14)
#3      OverlayState.insertAll (package:flutter/src/widgets/overlay.dart:346:5)
#4      OverlayRoute.install (package:flutter/src/widgets/routes.dart:43:24)
...

这是我创建登录按钮的函数,它是从 LoginScreen (StatelessWidget) 的 build 函数调用的

Widget loginButton(BuildContext context) 
    return Consumer<ApiCallChangeNotifier>(
        builder: (context, apiCall, child) => apiCall.isFetching
            ? CircularProgressIndicator()
            : apiCall.response != null
                ? Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(
                        builder: (context) => HomeScreen(
                            (apiCall.response as LoginResponse).email)))
                : RaisedButton( 
                     ...
                     onPressed: () 
                         attemptLogin(context);
                     ,
                     ...
                  ));
  

attemptLogin 函数:

void attemptLogin(BuildContext context) 
    Provider.of<ApiCallChangeNotifier>(context, listen: false).callApi(
        MyApiServices().attemptLogin,
        
          'email': emailController.value.text,
          'password': passwordController.value.text,
        ,
        urlController.value.text
    );
  

ApiCallChangeNotifier

class ApiCallChangeNotifier extends ChangeNotifier 
  bool isFetching = false;
  Object response;

  Future<LoginResponse> callApi(apiFunction, bodyParams, customUrl) async 
    isFetching = true;
    notifyListeners();

    response = await apiFunction(bodyParams, customUrl);

    isFetching = false;
    notifyListeners();
    return response;
  

MyApiServices.attemptLogin 是处理 API 调用并返回 Object LoginResponse 的函数

希望我已经提供了足够的信息!

【问题讨论】:

【参考方案1】:

对我来说,这是我在构建完成之前使用导航器的时候! 只需将您的导航代码放在这里:

WidgetsBinding.instance.addPostFrameCallback((_) 
  // Do everything you want here...
);

【讨论】:

【参考方案2】:

我没有尝试从 LoginResponse Consumer 推送新路由,而是修改了尝试登录()以等待结果并导航到新路由!

void attemptLogin(BuildContext context) async 
    LoginResponse _apiResponse =
        await Provider.of<ApiCallChangeNotifier>(context, listen: false)
            .callApi(
                MyApiServices().attemptLogin,
                
                  'email': emailController.value.text,
                  'password': passwordController.value.text,
                ,
                urlController.value.text);

    if (_apiResponse != null) 
      if (_apiResponse.email != null) 
        Navigator.pushReplacement(
            context,
            MaterialPageRoute(
                builder: (context) => HomeScreen(_apiResponse.email)));
       else if (_apiResponse.errorMessage != null) 
        Scaffold.of(context)
            .showSnackBar(SnackBar(content: Text(_apiResponse.errorMessage)));
       else 
        Scaffold.of(context).showSnackBar(
            SnackBar(content: Text(KanbanBOXApi().unknownErrorMessage)));
      
    
  

【讨论】:

我现在也有类似的问题。我在这个解决方案中看到的问题是你将上下文与根本不应该有上下文的东西耦合起来。最佳情况下,导航是您希望从 UI 中执行的操作(假设您需要上下文)。其余的逻辑应该分开。

以上是关于“在构建期间调用 setState() 或 markNeedsBuild()”错误尝试在消费者小部件(提供程序包)内的导航器中推送替换的主要内容,如果未能解决你的问题,请参考以下文章