Flutter: setState() or markNeedsBuild() called during build from onPressed - flutter

I am trying to show a dialog when someone clicks on the trailing icon of a ListTile but I am getting the "setState() or markNeedsBuild() called during build" error message. Here are the cards (in a MyCard class) and the function which is giving me the error message (checkCard):
static Widget buildCard(MyCard card, BuildContext context) {
var dateFormat = DateFormat('MM/dd/yyyy');
return Column(
children: [
Align(
alignment: Alignment.centerRight,
child: Text(dateFormat.format(card.createdOn.toDate()))),
const SizedBox(height: 6),
ListTile(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
tileColor: Colors.white,
leading: CircleAvatar(child: Text(card.subCategory)),
title: Text("Score: " + card.score + " Misses: " + card.misses),
subtitle: card.comment.isNotEmpty
? Text("Comment(s): " + card.comment)
: null,
trailing: IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: checkCard(card, context)),
),
const SizedBox(height: 18),
],
);
}
static checkCard(MyCard card, BuildContext context) {
showDialog(context: context, builder: (context) => const Text("Hello"));
}
and they get built from StatefulWidgets as follows:
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(3, Utils.ddfDropdown)!
.whenComplete(() => setState(() {
isLoading = false;
})),
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return FutureBuilder<List<MyCard>>(
future: MyCard.readData(snapshot.data),
builder: (context, cards) {
if (cards.hasData) {
final card = cards.data!;
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: card.length,
itemBuilder: (context, index) {
return MyCard.buildCard(card[index], context); //Cards being built here
},
),
);
}
I looked up this error elsewhere; one suggestion was to do the following:
static checkCard(MyCard card, BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(context: context, builder: (context) => const Text("Hello"));
});
}
but it seems like doing that calls the showDialog without having to click on the trailing IconButton and the screen then simply goes black.
Another suggestion was to do something like:
static checkCard(MyCard card, BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback((_) {
showDialog(context: context, builder: (context) => const Text("Hello"));
});
}
but the same thing happens (it seems like showDialog gets called without having to click and the screen goes black).

The issue is coming from, calling the method while building the widget.
onPressed: checkCard(card, context)),
You like to call the method when it is pressed, so it can be
onPressed:()=> checkCard(card, context)),

Related

Issue with statefulWidget unable to make desired changes

I am working on a statefulWidget and my purpose is to make sure that the next button is not clickable until an option (in this language is selected). However it doesn't seem to work, I also added Yaesin's(Someone who answered) answer to the code
ListView.builder(
itemCount: histoires.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
histoires[index].title,
style: TextStyle(color: Colors.pink),
),
trailing: IconButton(
icon: Icon(Icons.play_arrow),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) =>
AlertDialog(
content: Column(children: [
InkWell(
onTap: () {
_handleTap;
},
child: ListTile(
trailing: Icon(Icons
.flag_circle_rounded),
title: Text(
"French",
style: TextStyle(
color: Colors
.blueGrey),
))),
_active
? InkWell(
onTap: () {},
child: Image.asset(
"assets/nextactive.png",
height: height * 0.2,
width: width * 0.4),
)
: Image.asset(
"assets/nextinactive.png",
height: height * 0,
width: width * 0)
]),
));
});
}));
}),
To update dialog UI, you can use StatefulBuilder's setState
return StatefulBuilder(
builder: (context, setState) =>
AlertDialog(
content: Column(children: [
While using separate method, pass the StatefulBuilder's setState to the function. For your case, it will be
onPressed: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setStateSB) => AlertDialog(
content: Column(children: [
InkWell(
onTap: () {
_handleTap(setStateSB);
},
child: ListTile(
Also make sure to receive this setStateSB(renamed to avoid confusion with state's setState).
_handleTap(setStateSB){ ....
More about using StatefulBuilder
Since your in a Dialog, for setState to work, you need to wrap it with a StatefulBuilder.
You haven't included your full code, so I'm using this example taken from the docs:
await showDialog<void>(
context: context,
builder: (BuildContext context) {
int? selectedRadio = 0;
return AlertDialog(
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(4, (int index) {
return Radio<int>(
value: index,
groupValue: selectedRadio,
onChanged: (int? value) {
setState(() => selectedRadio = value);
},
);
}),
);
},
),
);
},
);
See also
A YouTube video by the Flutter team explaining StatefulBuilder

How to enable/disable dialog buttons (actions)

I have a helper function to create dialogs in my flutter app:
Future<void> showContentDialog(BuildContext context,
{required Widget content, String? title, List<Tuple2<String, void Function()>>? actions}) async {
Widget? titleWidget;
if (title != null) {
titleWidget = Text(
title,
style: Theme.of(context).textTheme.titleSmall!.copyWith(fontWeight: FontWeight.bold),
);
}
var dialogActions = <Widget>[];
if (actions != null) {
dialogActions.addAll(
actions.map(
(a) => TextButton(
child: Text(a.item1),
onPressed: () {
a.item2();
}),
),
);
}
await showDialog(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
title: titleWidget,
content: SizedBox(width: ThemeHelpers.maxPopupWidth, child: content),
actions: dialogActions,
),
);
}
There is another similar one that is used on Apple devices that uses equivalent widgets.
I can easyly manage state on the content portion of the dialog by wrapping it in a StatefulBuilder, but how can I enable and disable the dialog buttons (the actions passed to the AlertDialog) builder depending on content state?
My first idea was to add another a ValueNotifier parameter to the action builders and wrap them in ValueListenerBuilders but that didn't work.
Do I have any way of doing that other than including the actions as buttons inside the content (were I can easyly manage their state)?
You can pass null on onPressed to disable the button state. While it is not clear from where you like to controll the state, you can use ValueNotifier, and it work for all widget
final ValueNotifier<bool> enableButton = ValueNotifier(false);
Future<void> showContentDialog(
BuildContext context,
) async {
await showDialog(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
content: SizedBox(
width: 222,
child: Column(
children: [
Text("A"),
ElevatedButton(
onPressed: () {
enableButton.value = !enableButton.value;
},
child: Text("toggleButtonState"),
)
],
),
),
actions: [
ValueListenableBuilder<bool>(
valueListenable: enableButton,
builder: (context, value, child) => ElevatedButton(
onPressed: value ? () {} : null,
child: Text("BTN"),
),
),
],
),
);
}
In content add column and add dialog widgets in it
await showDialog(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
title: titleWidget,
content: Column(
mainAxisSize: MainAxisSize.min,
children : [
SizedBox(width: ThemeHelpers.maxPopupWidth, child: content),
dialogActions,
]
)
),
);

Make showCupertinoModalPopup to return a value

Flutter reference documentation shows an example of using CupertinoPicker class wrapped by CupertinoModalPopupRoute popup Dialog:
https://api.flutter.dev/flutter/cupertino/CupertinoPicker-class.html (dartpad snippet available)
The Dialog with a CupertinoPicker pops up, once a button is clicked. Button's onPressed calls:
void _showDialog(Widget child) {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
// some UI setting omitted
child: SafeArea(
top: false,
child: child,
),
));
}
where child is:
CupertinoPicker(
// some UI setting omitted
onSelectedItemChanged: (int selectedItem) {
setState(() {
_selectedFruit = selectedItem;
});
},
children:
List<Widget>.generate(_fruitNames.length, (int index) {
return Center(
child: Text(
_fruitNames[index],
),
);
}),
),
Method showCupertinoModalPopup used to push a Dialog with a Picker just above the main page.
Method's annotation mentions that "Content below the widget is dimmed with a [ModalBarrier]", and exactly ModalBarrier's handleDismiss method is called once user taps outside the Dialog's area, this leads to Navigator.pop being called, which pops the Dialog returning default void value (I cannot find a way to pass custom callback to the ModalBarrier)
Again from the showCupertinoModalPopup's annotation:
"Returns a Future that resolves to the value that was passed to
[Navigator.pop] when the popup was closed."
So the question is, what is the right way (if any) to override Navigator.pop to make showCupertinoModalPopup return a value? I don't want to use Picker's onSelectedItemChanged as it constantly changes the state while user choses right value.
Instead I would use it like:
Future<void> _showDialog(Widget child) async {
int? value = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) => Container(
//omitted
child: SafeArea(
top: false,
child: child,
),
));
setState(() {
Provider.of<FruitsController>(context, listen: false).currentFruit = value;
});
}
I would appreciate your help.
Yes you can create a button to pop from showCupertinoModalPopup.
Future<void> _showDialog() async {
int? value = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) => Column(
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(-2); //pasing value on pop is -2
},
child: Text("Close ")),
Container(
child: SafeArea(
top: false,
child: CupertinoPicker(
onSelectedItemChanged: (int selectedItem) {
_selectedFruit = _fruitNames[selectedItem];
},
itemExtent: 33,
children:
List<Widget>.generate(_fruitNames.length, (int index) {
return Center(
child: Text(
_fruitNames[index],
),
);
}),
),
),
),
],
),
);
print(value);
}

How to dynamically change the bottomsheet height based on the lenght of the list in flutter

I have to use showModalBottomSheet for displaying list of menu item. But I am unable to dynamically set the height of the bottomsheet. Please help me to solve this issue. Thankyou.
This is how you can dynamically change the bottomsheet height based on the length of the list in the bottom sheet. But be aware of that you need to use statefulbuilder in the showModalBottomSheet to be able to setState the list.
List<int> myList = [1,2,3,4,5,6];
#override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('showModalBottomSheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, _setState){
return Container(
height: myList.length * 100,
color: Colors.amber,
child: ListView.builder(
itemCount: myList.length,
itemBuilder: (BuildContext ctxt, int index) {
return Row(
children: [
Text(myList[index].toString()),
TextButton(
onPressed: () {
_setState((){
myList = myList.where((value) => value != myList[index]).toList();
});
},
child: Text("delete"),
)
]);
}
)
);
}
);
},
);
},
),
);
}

How to refresh a ListViewBuilder when to click on a butotn?

I want to filter a listview by id or author using provider after i click on RaisedButton, but nothing seems to happen, that's what id did:
The refreshed button
#override
Widget build(BuildContext context) {
final todo_provider = Provider.of<TodoProvider>(context);
// TODO: implement build
return Scaffold(
body: Column(
children: <Widget>[
RaisedButton(
child: Text('Filter me'),
onPressed: () {
todo_provider.settNewss('John Biggs');
},
),
],
),
);
}
The result in ListView
#override
Widget build(BuildContext context) {
final todo_provider = Provider.of<TodoProvider>(context);
// TODO: implement build
return Container(
child: FutureBuilder<News>(
future: _future,
initialData: todo_provider.getnews,
builder: (context, snapshot) =>
snapshot.connectionState == ConnectionState.none ||
snapshot.data == null
? CircularProgressIndicator()
: Container(
height: 500.0,
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.articles.length,
itemBuilder: (context, index) => Text(
snapshot.data.articles[index].author.toString(),
),
),
),
),
);
}
try to wrap your onPressed function by a setState
onPressed: () {
setState() {
todo_provider.settNewss('John Biggs');
}
},
When using provider, to update data back to your Consumer you have to use
notifyListener();
Thus, under your settNewss() method, you probably need to use notifyListener() in the end to update the Consumer who are listening to this.
Tips: If you dont want to listen to the Provider on certain screens, you can use
final todo_provider = Provider.of<TodoProvider>(context, listen: false);
By doing this, your notifyListener() will not notify every widget that need this data.
I finally found a solution, is to update the future value on FutureBuilder:
OLD:
onPressed: () {
todo_provider.settNewss('John Biggs');
}
NEW:
onPressed: () {
_future = usersProvider.fetchUsers('John Biggs');
}
OR:
onPressed: () {
_future = Provider.of<TodoProvider>(context, listen: false)
.fetchUsers('John Biggs');
}