Calling showDialog in didChangeDependencies causes Error: setState() called during build

1,748 views
Skip to first unread message

mi...@treo.co

unread,
Oct 27, 2018, 10:09:04 PM10/27/18
to Flutter Dev
Greetings,

I've been attempting to add a pop-up dialog via showDialog when certain error conditions arise -- in order to inform the user before restarting the application.  Right now the presence of an error state is being tracked by an InheritedWidget.  Therefore, the natural place to call showDialog is from my widget's didChangeDependencies method; however, doing so results in an error because didChangeDependencies is apparently called while building the inherited widget, and showDialog calls NavigatorState.push, which ultimately causes an instance of OverlayState to call setState, causing an error with message "setState() or markNeedsBuild() called during build.".

This has lead me to a few questions that I can't readily find answers to:

1) Is this the intended behavior of showDialog for this scenario, or is it a bug? In other words, is it essentially wrong to try to call showDialog from didChangeDependencies? I couldn't find any documentation indicating this.  
2) What would be a good way to avoid this specific problem?  I found that one potential option is to use WidgetsBinding.instance.addPostFrameCallback to schedule the call to showDialog, but it is not immediately obvious how to simply and efficiently pass the (Future) return value of showDialog back to the caller.  It also seems to work by adding "await Future.delayed(Duration(microseconds: 1));" before the call to showDialog, but this seems pretty hacky, and I'm not sure whether it guarantees correct behavior.
3) Is this a symptom of a more fundamental problem with my approaches of using InheritedWidgets for state and relaying errors via showDialog?  I wonder since I haven't seen others report this issue, so I wonder if perhaps I'm not taking a conventional approach to this. 
 
This happens on both beta (0.9.4) and master (0.10.2).  The full stack trace and example application that produces the behavior are below.  Much appreciative of any help.

Regards,

Mike

```
E/flutter ( 5407): [ERROR:flutter/shell/common/shell.cc(188)] Dart Error: Unhandled exception:
E/flutter ( 5407): setState() or markNeedsBuild() called during build.
E/flutter ( 5407): 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.
E/flutter ( 5407): The widget on which setState() or markNeedsBuild() was called was:
E/flutter ( 5407):   Overlay-[LabeledGlobalKey<OverlayState>#25ef6](state: OverlayState#adedc(entries: [OverlayEntry#24535(opaque: false; maintainState: false), OverlayEntry#b0db4(opaque: false; maintainState: true), OverlayEntry#242aa(opaque: false; maintainState: false), OverlayEntry#4f22f(opaque: false; maintainState: true)]))
E/flutter ( 5407): The widget which was currently being built when the offending call was made was:
E/flutter ( 5407):   ApplicationRootWidget(state: ApplicationRootWidgetState#10e58)
E/flutter ( 5407): #0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:3471:11)
E/flutter ( 5407): #1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:3497:6)
E/flutter ( 5407): #2      State.setState (package:flutter/src/widgets/framework.dart:1140:14)
E/flutter ( 5407): #3      OverlayState.insertAll (package:flutter/src/widgets/overlay.dart:301:5)
E/flutter ( 5407): #4      OverlayRoute.install (package:flutter/src/widgets/routes.dart:43:24)
E/flutter ( 5407): #5      TransitionRoute.install (package:flutter/src/widgets/routes.dart:185:11)
E/flutter ( 5407): #6      ModalRoute.install (package:flutter/src/widgets/routes.dart:862:11)
E/flutter ( 5407): #7      NavigatorState.push (package:flutter/src/widgets/navigator.dart:1538:11)
E/flutter ( 5407): #8      showGeneralDialog (package:flutter/src/widgets/routes.dart:1523:53)
E/flutter ( 5407): #9      showDialog (package:flutter/src/material/dialog.dart:607:10)
E/flutter ( 5407): #10     WidgetWithAlertState._showErrorAlert (package:frozen_list_views_bug_app/main.dart:53:27)
E/flutter ( 5407): <asynchronous suspension>
E/flutter ( 5407): #11     WidgetWithAlertState.didChangeDependencies (package:frozen_list_views_bug_app/main.dart:47:7)
E/flutter ( 5407): #12     StatefulElement.didChangeDependencies (package:flutter/src/widgets/framework.dart:3916:12)
E/flutter ( 5407): #13     InheritedElement.notifyDependent (package:flutter/src/widgets/framework.dart:4183:15)
E/flutter ( 5407): #14     InheritedElement.notifyClients (package:flutter/src/widgets/framework.dart:4212:7)
E/flutter ( 5407): #15     ProxyElement.update (package:flutter/src/widgets/framework.dart:3944:5)
E/flutter ( 5407): #16     Element.updateChild (package:flutter/src/widgets/framework.dart:2728:15)
E/flutter ( 5407): #17     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3688:16)
E/flutter ( 5407): #18     Element.rebuild (package:flutter/src/widgets/framework.dart:3530:5)
E/flutter ( 5407): #19     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2272:33)
E/flutter ( 5407): #20     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:673:20)
E/flutter ( 5407): #21     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:219:5)
E/flutter ( 5407): #22     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:990:15)
E/flutter ( 5407): #23     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:930:9)
E/flutter ( 5407): #24     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:842:5)
E/flutter ( 5407): #25     _invoke (dart:ui/hooks.dart:145:13)
E/flutter ( 5407): #26     _drawFrame (dart:ui/hooks.dart:134:3)
```

import 'dart:core';

import 'package:flutter/material.dart';

class AlertDialogApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    MaterialApp matApp = new MaterialApp(
      home: ApplicationRootWidget(child: (WidgetWithAlert())),
    );

    return matApp;
  }
}

void main() {
  runApp(AlertDialogApp());
}


class WidgetWithAlert extends StatefulWidget {
  WidgetWithAlert();

  @override
  WidgetWithAlertState createState() {
    return new WidgetWithAlertState();
  }
}

class WidgetWithAlertState extends State<WidgetWithAlert> {
  @override
  void initState() {
    super.initState();
  }


  @override
  void didChangeDependencies() {
    if(ApplicationRootWidget.of(context).errorOccurred) {
       //  Error can be mitigated by substituting these two lines for the next line:
       // WidgetsBinding.instance
       //     .addPostFrameCallback((_) => _showErrorAlert(context));     // Unclear how to retrieve the return value
      _showErrorAlert(context);
    }
    super.didChangeDependencies();
  }

  Future<bool> _showErrorAlert(context) async {
    Future<bool> result = showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) => AlertDialog(title: Text("An Error Occurred"),
        content: Text("An unexpected error has occurred and we must restart the application.  "
            "Would you like to send a crash report before restarting, or just restart?"),
        actions: [
          FlatButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: Text("Send Crash Report"),),
          FlatButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: Text("Just Restart"),),
        ]
      )
    );

    return result;
  }

  @override
  Widget build(BuildContext context) {

    return Container(child:

    Text("Hello World. All is well."));
  }
}


class _InheritedApplicationRootWidgetState extends InheritedWidget {
  final ApplicationRootWidgetState data;

  const _InheritedApplicationRootWidgetState({
    Key key,
    @required this.data,
    @required Widget child,
  })
      : assert(child != null),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_InheritedApplicationRootWidgetState old) {
    return true;
  }
}


class ApplicationRootWidget extends StatefulWidget {
  // You must pass through a child.
  final Widget child;

  ApplicationRootWidget({
    @required this.child,
  });

  @override
  ApplicationRootWidgetState createState() => ApplicationRootWidgetState();

  static ApplicationRootWidgetState of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(_InheritedApplicationRootWidgetState)
    as _InheritedApplicationRootWidgetState).data;
  }
}


class ApplicationRootWidgetState extends State<ApplicationRootWidget> {
  bool errorOccurred = false;

  @override
  void initState() {
    super.initState();
    changeToError();
  }

  Future changeToError() async {
    await Future.delayed(Duration(seconds: 2));
    setState(() {
      errorOccurred = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return _InheritedApplicationRootWidgetState(data: this,
      child: widget.child,
    );
  }
}

Kris Giesing

unread,
Oct 28, 2018, 1:22:38 AM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
One would typically call showDialog from a build() method. I think perhaps the root of the issue here is the override of didChangeDependencies; I think you can simply move the block of code there to the WidgetWithAlertState.build() method, and putting the existing contents in an else block.

I'm guessing that making WidgetWithAlertState stateful was done in order to gain access to the context member variable. If so, then once the code is moved, I believe you should be able to make it stateless, and use the context passed in to the build() method instead.

To answer your specific questions:

1) I believe this is the desired behavior in the sense that didChangeDependencies isn't the place to build widgets, which is what showDialog ends up doing.
2) I think the suggestion above would fix the issue, but it's possible I'm mis-reading the code or otherwise missing something. I'd be curious to know whether it works.
3) Using InheritedWidget to broadcast error state is probably OK fundamentally, but there are nuances and details. I mentioned the use of StatelessWidget for the clients of InheritedWidget as one such nuance. Another is your use of a delay between receiving a notification to change to an error state and the call to setState(); I'm guessing that's to simulate an external asynchronous call. When doing such operations from a State object, one needs to be careful about the State object's lifecycle; if the State object is torn down then any outstanding async calls need to be dealt with somehow. Here, that's probably OK because this is a root widget that is probably never torn down, but I would be careful copying this pattern into a deeper part of the hierarchy.

InheritedWidget is a really interesting class; it's not really like anything else I've encountered in other UI frameworks, and it took me a long time to figure out what's it's doing under the covers. It seems kind of magical at first glance that a StatelessWidget can update when something it refers to in its build() method changes - that's something I previously believed required a StatefulWidget or something more complex. The reason it can do what it does is the context argument passed to the static .of() methods; BuildContext can set up dependencies between different locations in the tree, and the .of() methods have enough information about both locations to set up that dependency.

Hope this helps,

- Kris

--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ian Hickson

unread,
Oct 28, 2018, 4:30:44 AM10/28/18
to Kris Giesing, mi...@treo.co, flutt...@googlegroups.com
You can't show a dialog while initState, didChangeDependencies, or build are running, because to do so involves triggering a rebuild of the Navigator, which would have already built by the time your widget builds (the tree is built top-down). So it would require that the Navigator rebuild again, which would mean the entire tree basically would have to rebuild again, which would be quite inefficient.

In general builds are expected to be idempotent (meaning they can run again and again and nothing will change). showDialog is not idempotent, it changes the state of the app. As such, it should be run from an event handler. In this case, since the information is being sent via an InheritedWidget, there must be a moment where the InheritedWidget's parent changes the InheritedWidget. That happens during a build. That build is itself scheduled using setState. The point at which setState is called is the point at which showDialog should be called.
--

--
Ian Hickson

😸

mi...@treo.co

unread,
Oct 28, 2018, 9:34:00 AM10/28/18
to Flutter Dev
Thanks for helping Kris.  Pointing out the issue with outstanding async calls that modify the state after it's out of the widget tree is super helpful.  That's actually something that I'm still trying to identify a good design pattern for dealing with. Maybe I'll make another post on that topic later.... :)

Unfortunately, as Ian pointed out, showDialog can't be called from build either, since it would modify Navigator and trigger a setState while building, which is a no-no / no-go.

Regards,

Mike

Steven McDowall

unread,
Oct 28, 2018, 10:26:08 AM10/28/18
to mi...@treo.co, Flutter Dev

I think one of the largest issues is how to properly do async remote calls that fill in the data in a screen/widget set.. 

Especially when one also wants to have a Gesutre "Update" sort of thing too .. but NOT call the async (slow) data fetch every single "build()" cycle ... 

I'm struggling with this pattern as well and I hope there is some cookbook clarity somewhere on really the best way -- and do these Screens need to be "Stateful" if they are getting remote data asyn and updating the screen (from within itself -- say on a gesture?) or can we relaly use Stateless widgets somehow? 

This would be an excellent Medium (or whatever) post from someone who has some best practices here for this .. I know it would help me a ton and I have like 15 screens done in various ways -- and not happy about any of them. LOL

mi...@treo.co

unread,
Oct 28, 2018, 12:53:31 PM10/28/18
to Flutter Dev
Thanks Ian!  What you're saying makes total sense.  One other thing I realized is that I can and probably should move this particular InheritedWidget's parent even higher, where it will be an ancestor of the MaterialApp, in which case didChangeDependencies actually can modify the navigator because it has not yet been built/rebuilt.  This would leave me with some other problems though, like making sure that I call showDialog exactly once and only at the moment that this root widget's state changes (e.g. not when some other dependency deeper in the tree, beneath the Navigator, has changed).  Overall, I think the mess won't be worth it, so I think I may try using Streams for reacting to these types of error events.  I may also take a closer look at flutter-redux to see if that framework eliminates these concerns.    

Thanks,

Mike

Kris Giesing

unread,
Oct 28, 2018, 4:06:46 PM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
Apologies for the misinformation previously.

I think putting the showDialog call above the MaterialApp might not work because the dialog looks at the incoming context to retrieve the Navigator and Theme. In general, it's looking like InheritedWidget is not going to work for this, not because it's a poor choice for propagating error state generally, but because the reaction to InheritedWidget happens in build functions and showDialog needs to be called from outside the build sequence.

I played around with this a little bit and it seems like the closest analog to the current code base is to keep WidgetWithAlert a stateful widget, and have its state subscribe directly to changes in ApplicationRootWidgetState (via a Stream or a similar listener/subscriber interface). When the subscription is invoked, WidgetWithAlertState._showErrorAlert is called at that point, using the context that the WidgetWithAlert. However, it's a little unclear at that point why WidgetWithAlert is the thing in charge of posing the dialog; its contents don't seem particularly related to the dialog. It might be cleaner to create a stateful widget whose sole purpose is to serve as the point in the hierarchy where the dialog is meant to be hosted, and have its constructor take a child that it simply returns in its build method. (When implementing this pattern, care needs to be taken to unsubscribe from the stream in the stateful widget's dispose() and didUpdateWidget() methods.)

On the Flutter side, I wonder if showDialog's documentation could be improved to discuss when it's allowed to be called. The presence of a context argument in a parameter list suggested to me that it was meant to be called from build methods. There's some example code in the Flutter gallery, which helps, but I missed on first read that showDialog is being called from an event listener there rather than directly in build().

- Kris


Kris Giesing

unread,
Oct 28, 2018, 4:08:56 PM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
Sorry, incomplete sentence below:

"using the context that the WidgetWithAlert" => "using the context that the WidgetWithAlertState holds in its member variable".

mi...@treo.co

unread,
Oct 28, 2018, 8:08:56 PM10/28/18
to Flutter Dev
Thanks again Kris -- good tip on separating the widget that displays errors from the widget with the normal workflow.  I'm going to keep them together in this example only to try to minimize the number of layers in the widget tree.  Regarding my proposed fix, I think there was a miscommunication there -- I didn't mean to suggest putting showDialog above the MaterialApp, but rather to keep the showDialog where it is and move the ApplicationRootWidget above the MaterialApp.  

Specifically, I just moved ApplicationRootWidget from inside AlertDialogApp.build and into main(), where it wraps AlertDialogApp.  Doing so worked for me in my initial testing.  However, it turns out that this still has a problem -- it's just a less consistent one.  It will work fine as long as the WidgetWithAlert is created before the error state is set, but if the WidgetWithAlert is created afterwards, it still throws an error.  This happens because didChangeDependencies is also called at the start of a StatefulWidget's lifecycle in StatefulElement._firstBuild, and this happens after the MaterialApp's Navigator & Overlay has been built, so the error is thrown.  

I've included a revised version of the code below my signature, which works error-free unless you change the initial value of errorOccurred to true.  It's a little unsettling that this can work in the typical case but fail in an edge case.  I certainly will avoid using this myself, but seems like something that could create problems for others who may be unaware of the danger.  

This also seems like another good reason to add additional documentation like you suggested, in order to guide people away from this problem.  Although it seems that the issue is not limited to just showDialog, but rather to any code that attempts to modify the Navigator, directly or indirectly, so maybe such documentation would be more useful in the docs of didChangeDependencies (https://docs.flutter.io/flutter/widgets/State/didChangeDependencies.html ).  

Regards,

Mike

import 'dart:core';

import 'package:flutter/material.dart';

class AlertDialogApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    MaterialApp matApp = new MaterialApp(
      home: (WidgetWithAlert()),
    );

    return matApp;
  }
}

void main() {
  runApp(ApplicationRootWidget(child:AlertDialogApp()));
}

// For reference, here is the old version of the previous class and main method, which consistently throws an error.
//class AlertDialogApp extends StatelessWidget {
//  @override
//  Widget build(BuildContext context) {
//    MaterialApp matApp = new MaterialApp(
//      home: (ApplicationRootWidget(child: WidgetWithAlert())),
//    );
//
//    return matApp;
//  }
//}
//
//void main() {
//  runApp(AlertDialogApp());
//}
  // replacing the line above with the line below will cause an error to be thrown
//  bool errorOccurred = true;

  @override
  void initState() {
    super.initState();
    changeToError();
  }

  Future changeToError() async {
    await Future.delayed(Duration(microseconds: 2));

Kris Giesing

unread,
Oct 28, 2018, 8:34:06 PM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
Re: documentation, the issue seems to be between two classes of operations:

1) operations that occur during the widget build cycle, which includes build() and didUpdateDependencies() for sure, but also includes a lot of other methods
2) operations that are not allowed during the build cycle, but which require a BuildContext. Since the build method is one of the most common ways to get access to a BuildContext, it's a fairly non-obvious restriction.

Since there are a number of operations in each class, putting a full description of the issue in each one's API docs might be a lot of duplicated explanation. I wonder instead if there could be a deep dive doc in either the "Want to skill up?" or "Specialized topics" sections here: https://flutter.io/docs/.

I don't see an existing issue covering this documentation request so I think I'll go ahead and file a new one.

Kris Giesing

unread,
Oct 28, 2018, 8:34:55 PM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
Oh, I meant to also suggest that once a full description of the problem exists somewhere then the individual API doc entries could point to it.

Kris Giesing

unread,
Oct 28, 2018, 8:38:01 PM10/28/18
to mi...@treo.co, flutt...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages