Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the "setState called during build" in common-errors.md #11353

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 71 additions & 44 deletions src/content/testing/common-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,27 +443,48 @@ attempting to trigger a `Dialog` from within the
immediately show information to the user,
but `setState` should never be called from a `build` method.

The following snippet seems to be a common culprit of this error:
The following code illustrates a common culprit of this error:

<?code-excerpt "lib/set_state_build.dart (problem)"?>
```dart
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all the code required for running an app is being added here, maybe it could be structured as an interactive DartPad sample! I'm not sure exactly how it works but I did see another page that was formatted this way:

## Interactive example
<?code-excerpt "lib/main.dart"?>
```dartpad title="Flutter TabBar DartPad hands-on example" run="true"
import 'package:flutter/material.dart';
void main() {
runApp(const TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
const TabBarDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: const Text('Tabs Demo'),
),
body: const TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
);
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea, but I don't know how I'd be able to test the functionality before submitting it for review. I can get common-errors.md onto my PC, but how would I serve it to myself for testing, especially with the an interactive DartPad in place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal is to test out your sample code, feel free to copy-paste it into the existing TabBar demo :)

Widget build(BuildContext context) {
// Don't do this.
showDialog(
import 'package:flutter/material.dart';

void main() => runApp(const ShowDialogExampleApp());

class ShowDialogExampleApp extends StatelessWidget {
const ShowDialogExampleApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const DialogExample(),
);
554richard marked this conversation as resolved.
Show resolved Hide resolved
}
}

class DialogExample extends StatelessWidget {
const DialogExample({super.key});

@override
Widget build(BuildContext context) {
// Don't do this:
showDialog(
context: context,
builder: (context) {
builder: (BuildContext context) {
return const AlertDialog(
title: Text('Alert Dialog'),
);
});

return const Center(
child: Column(
children: <Widget>[
Text('Show Material Dialog'),
],
),
);
},
);
return Scaffold(
appBar: AppBar(title: const Text('This does not work')),
body: const Center(
child: Text('Does not Work'),
),
);
}
}
```

Expand All @@ -475,43 +496,49 @@ framework for every frame, for example, during an animation.

**How to fix it?**

One way to avoid this error is to use the `Navigator` API
to trigger the dialog as a route. In the following example,
there are two pages. The second page has a
dialog to be displayed upon entry.
When the user requests the second page by
clicking a button on the first page,
the `Navigator` pushes two routes–one
for the second page and another for the dialog.
One way to avoid this error is instruct flutter to finish rendering the page
before implementing the showDialog. This can be done by using
554richard marked this conversation as resolved.
Show resolved Hide resolved
addPostFrameCallback() method. The following code illustrates this on the broken example just given:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally we want files to have lines of <= 80 characters. (more info here)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split line 501 into two in my fork of the code, but I'm not sure if this change makes its way automatically into the PR, or whether I need to do something to update it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this PR is using the 554richard:patch-1 branch. If you push changes to that branch, they'll show up in this pull request!


<?code-excerpt "lib/set_state_build.dart (solution)"?>
```dart
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
import 'package:flutter/material.dart';

void main() => runApp(const ShowDialogExampleApp());

class ShowDialogExampleApp extends StatelessWidget {
const ShowDialogExampleApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const DialogExample(),
);
}
}

class DialogExample extends StatelessWidget {
const DialogExample({super.key});

@override
Widget build(BuildContext context) {
//You can do this:
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return const AlertDialog(
title: Text('Alert Dialog'),
);
},
);
});
Comment on lines +507 to +517
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the post-frame callback suggestion!

In its current state, though, I'm not sure if this is something we want to encourage in the documentation: scheduling a dialog in the build method means a new dialog is created each time the widget is rebuilt. If there's something like Theme.of(context) or MediaQuery.sizeOf(context) in the build method, resizing the window or switching to dark mode might make a new dialog pop up every frame!

The ideal setup would be to use a stateful widget (so the dialog could be scheduled inside initState) or make it part of an onPressed callback (similar what the original example did). Perhaps this page could show both!

return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: ElevatedButton(
child: const Text('Launch screen'),
onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
// Immediately show a dialog upon loading the second screen.
Navigator.push(
context,
PageRouteBuilder(
barrierDismissible: true,
opaque: false,
pageBuilder: (_, anim1, anim2) => const MyDialog(),
),
);
},
Comment on lines -501 to -513
Copy link
Member

@nate-thegrate nate-thegrate Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a possible solution that doesn't involve stateful widgets (using the Navigator's context for the dialog, to ensure it isn't disposed of by the end of the frame):

onPressed: () async {
  final NavigatorState navigator = Navigator.of(context);
  navigator.pushNamed('/second');

  // The default Material page transition duration is 300 ms.
  await Future.delayed(Durations.medium2);
  showDialog(
    context: navigator.context,
    builder: (context) => const MyDialog(),
  );
},

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difficulty I'm having with implementing this is that my example code doesn't have an onPressed callback anywhere in it, and if I create a button in the Scaffold and put the showDialog in its onPressed:,, the "setState error during build" error disappears without the need of a addPostFrameCallback.

I'm afraid that the broken code I give as an example is crazy - nobody in their right mind would write it. The actual code that caused me the problem was with pluto_grid, and I don't know how to create a sensible example of the error without it - and I can see that pluto_grid should not be included in a page like this. I guess the solution snippet in the current version is a good solution to a problem which was also too specific - which is why I didn't find it helpful.

I suppose I've got into trouble because I'm trying to post a solution to a specific problem in a "common errors" page whose purpose has to be much more general.

If I can't find a sensible example of broken code - which uses a callback like onPressed or onChanged and causes this particular error without using pluto_grid, I don't think I can make a sensible contribution. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing your thoughts.

I think you made a good point about the post-frame callback not being necessary here 🙂
I edited the comment above accordingly, thanks!

),
appBar: AppBar(title: const Text('This works')),
body: const Center(
child: Text('It Works'),
),
);
}
Expand Down