Reputation: 85
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.
Upvotes: 0
Views: 2258
Reputation:
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));
Upvotes: 1