0

I'd like to close a showModalBottomSheet when a boolean condition is verified as true in the code. The intended behavior is working, though, on the debug console of VSCode I'm seeing an exception being thrown, and I'm afraid it can lead to certain bugs when it's deployed to production later on. The exception is the following:

════════ Exception caught by animation library ═════════════════════════════════
The following assertion was thrown while notifying status listeners for AnimationController:
setState() or markNeedsBuild() called during build.

This _ModalScope<void> 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: _ModalScope<void>-[LabeledGlobalKey<_ModalScopeState<void>>#8025f]
    state: _ModalScopeState<void>#76437
The widget which was currently being built when the offending call was made was: Observer
    dirty
When the exception was thrown, this was the stack
#0      Element.markNeedsBuild.<anonymous closure> 
package:flutter/…/widgets/framework.dart:4167
#1      Element.markNeedsBuild 
package:flutter/…/widgets/framework.dart:4182
#2      State.setState 
package:flutter/…/widgets/framework.dart:1253
#3      _ModalScopeState._routeSetState 
package:flutter/…/widgets/routes.dart:759
#4      ModalRoute.setState 
package:flutter/…/widgets/routes.dart:878
...
The AnimationController notifying status listeners was: AnimationController#6cac6(◀ 1.000; for BottomSheet)

I'm using Mobx as my state management tool, and when a computed value becomes true, I want the showModalBottomSheet to close.

The showModalBottomSheet code is shown below where you can find the Navigator.pop(context, true) call in the builder method of the Observer:

void _addGroupBottomSheet(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;

    showModalBottomSheet<void>(
      // enableDrag: true,
      elevation: 5,
      isScrollControlled:
          true, // make it bigger, being able to fill the whole screen
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(30),
          topRight: Radius.circular(30),
        ),
      ),
      // backgroundColor: Theme.of(context).primaryColor,
      backgroundColor: Colors.amber,
      context: context,
      builder: (BuildContext context) {
        final store = Provider.of<GroupStore>(context, listen: false);

        return GestureDetector(
          onTap: () {
            FocusScopeNode currentFocus = FocusScope.of(context);

            if (!currentFocus.hasPrimaryFocus) {
              currentFocus.unfocus();
            }
          },
          child: Observer(builder: (_) {

            List<Group> allGroups = store.listOfAllGroupsSelected;

            bool isGroupFull = widget.isGroupA
                ? store.isGroupAFull
                : store.isGroupBFull;

            // close bottomSheet programmatically when condition satisfies
            if (isGroupFull) {
              Navigator.pop(context, true);
            }

            return Padding(
              padding: const EdgeInsets.only(bottom: 12.0),
              child: Container(
                height: screenSize.height * 0.8,
                // color: Colors.amber,

                child: Column(
                  // mainAxisAlignment: MainAxisAlignment.center,
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Text(
                      'Looking at ${widget.isGroupA ? 'group A' : 'group B'}',
                      style: TextStyle(
                        fontSize: 20,
                      ),
                    ),
                    Expanded(
                      child: ListView.separated(
                        itemCount: allGroups.length,
                        physics: const BouncingScrollPhysics(),
                        separatorBuilder: (BuildContext context, int index) =>
                            Divider(),
                        itemBuilder: (BuildContext context, int index) {
                          return ListTile(
                            key: Key(index.toString()),
                            // dense: true,
                            title: Text('${allGroups[index].name}'),
                            leading: ContainerAvatar(
                              url: '${allGroups[index].imageUrl}',
                            ),
                            trailing: allGroups[index].isSelected 
                                ? null
                                : FlatButton(
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(18.0),
                                      side: BorderSide(
                                          color: Theme.of(context).accentColor),
                                    ),
                                    onPressed: () {

                                      FocusScopeNode currentFocus =
                                          FocusScope.of(context);
                                      if (!currentFocus.hasPrimaryFocus) {
                                        currentFocus.unfocus();
                                      }

                                      if (widget.isGroupA) {
                                        if (allGroups[index].isSelected) {
                                          store.modifyEnemyTeamList(
                                            name: allGroups[index].name,
                                            operation: 'Remove',
                                          );
                                        } else {
                                          // add to the enemy team list
                                          store.modifyEnemyTeamList(
                                            name: allGroups[index].name,
                                            operation: 'Add',
                                          );
                                        }
                                      } 
                                    },
                                    child: Text(
                                      '${allGroups[index].isSelected ? 'Remove' : 'Add'}',
                                    ),
                                  ),
                          );
                        },
                      ),
                    ),
                    // RaisedButton(
                    //   child: const Text('Close BottomSheet'),
                    //   onPressed: () => Navigator.pop(context),
                    // )
                  ],
                ),
              ),
            );
          }),
        );
      },
    );
  }

I've already checked some other similar questions and their solutions, such as this one, but I couldn't get this working without the exception mentioned above.

So, how can I achieve the intended behavior (close the modal when the computed value evalutes true) without getting the exception being thrown?

PS: If I move the logic to close showModalBottomSheet to the onPressed callback of FlatButton it doesn't throw the exception, though, it allows the insertion of one additional widget beyond the desired number as the check if it is full or not will only be made in the next state update (I guess), that's why I'm inserting the verification up in the builder method, before its return, but in turn, receiving an exception in the debug console.

PS2: If I'm doing something considered a bad practice please let me know as well.

satler
  • 75
  • 3
  • 11

1 Answers1

1

You shouldn't be calling Navigator.pop(context) while building a widget. You can execute it after the build is complete with this line of code:

WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.pop(context));
Shahin Mursalov
  • 625
  • 8
  • 10
  • Replacing my call with this piece of code does close the modalBottomSheet but also the whole route/screen, leaving the user in a black screen. I tried moving this up in the widget tree, outside of the `addGroupBottomSheet` method and it closed the modal. Though, the route the user was in was also closed. so he ended up in the very first screen (1st screen -> 2nd -> Modal). I might doing something wrong or anti-pattern here to get these results. In the end, I modified a bit the logic and was able to close it from inside the FlatButton. All in all, thank you for taking the time to answer this! – satler Jul 13 '20 at 20:09