"setState() or markNeedsBuild() called during build" error trying to push a replacement in Navigator inside a Consumer widget (provider package)
Asked Answered
A

3

8

This week I've began developing in flutter and i'm not able to solve this problem.

I'm building a login page that calls an API to login and after redirects to an homepage. This is the exception generated by Navigator.pushReplacement in the first code bloc. In that moment apiCall.isFetching is false cause fetching ended and apiCall.response contains the required data.

Exception details:

════════ 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)
...

Here is my function to create the login button, it's called from build function of LoginScreen (StatelessWidget)

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);
                     },
                     ...
                  ));
  }

The attemptLogin funtion:

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 is a function that handles the API call and returns an Object LoginResponse

Hope I've given enough info!

Architect answered 20/9, 2019 at 14:30 Comment(0)
A
3

Instead of trying to push the new route from LoginResponse Consumer I modified attemptLogin() to wait the result and to navigate to the new route!

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)));
      }
    }
  }
Architect answered 23/9, 2019 at 14:51 Comment(1)
I am having a similar issue now. The problem I see with this solution is that you are coupling the context with something that shouldn't have a context at all. Optimally the navigation is something you want to do from the UI (given that you need a context). The rest of the logic should be separated.Sherlock
A
10

For me, It was when I use navigator before the build finished! Just put your navigation code in here:

WidgetsBinding.instance.addPostFrameCallback((_) {
  // Do everything you want here...
});
Annapurna answered 14/8, 2020 at 8:21 Comment(0)
A
3

Instead of trying to push the new route from LoginResponse Consumer I modified attemptLogin() to wait the result and to navigate to the new route!

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)));
      }
    }
  }
Architect answered 23/9, 2019 at 14:51 Comment(1)
I am having a similar issue now. The problem I see with this solution is that you are coupling the context with something that shouldn't have a context at all. Optimally the navigation is something you want to do from the UI (given that you need a context). The rest of the logic should be separated.Sherlock
C
0

i have face the same issue and resolve using SchedulerBinding this is work for me.

 SchedulerBinding.instance.addPostFrameCallback((_) {
    //Enter here your popup or navigation code
  });
Carburetor answered 16/1 at 11:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.