David Serrano Canales
David Serrano

David Serrano

Flutter PRO tip #0: Open a dialog at startup

Flutter PRO tip #0: Open a dialog at startup

David Serrano Canales
·May 5, 2022·

3 min read

From time to time one needs to show a dialog just when the user opens a new screen, it's possible that certain requirements are met to show them valuable information or perhaps you have to warn them about some relevant fact in your application; and given its relevance, you may have decided to display a dialog just when the user opens the screen.

📽 Video version available on YouTube and Odysee

If your widget is a StatefulWidget, maybe you have thought on doing something like that:

class SomeWidget extends StatefulWidget {
  const SomeWidget({Key? key}) : super(key: key);

  @override
  State<SomeWidget> createState() => _SomeWidgetState();
}

class _SomeWidgetState extends State<SomeWidget> {
  @override
  void initState() {
    super.initState();

    // Try to open a dialog in initState()
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        content: const Text('Dialog content'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Accept'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sample'),
      ),
      body: const Center(
        child: Text('Body of the Scaffold'),
      ),
    );
  }
}

But then you get an error like this:

======== Exception caught by widgets library ======================================================= The following assertion was thrown building Builder: dependOnInheritedWidgetOfExactType() or dependOnInheritedElement() was called before _PostFrameCallbackSampleState.initState() completed.

When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.

Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.

The reason why this happens is because that widget is in the rendering phase, so to show the dialog we have to wait for that phase to finish. How can we do it? Well, luckily in the WidgetsBinding class we have a method called addPostFrameCallback, whose documentation tells us the following:

Schedule a callback for the end of this frame.

Through this method we can 'schedule' that operation so that it is executed at the end of the rendering of the current frame, being as follows:

class PostFrameCallbackSample extends StatefulWidget {
  const PostFrameCallbackSample({Key? key}) : super(key: key);

  @override
  State<PostFrameCallbackSample> createState() =>
      _PostFrameCallbackSampleState();
}

class _PostFrameCallbackSampleState extends State<PostFrameCallbackSample> {
  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance?.addPostFrameCallback((_) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          content: const Text('Dialog content'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Accept'),
            ),
          ],
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Post Frame Callback sample'),
      ),
      body: const Center(
        child: Text('Body of the Scaffold'),
      ),
    );
  }
}

This way we can display a dialog without problems at the beginning of the lifecycle of this widget. In fact, it can also be used for any other operation that requires changes to the UI, apart from showing a dialog.

In case your widget is a StatelessWidget we could wrap it in a FutureBuilder like this:

class PostFrameCallbackSampleStateless extends StatelessWidget {
  const PostFrameCallbackSampleStateless();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _showDialog(context),
      builder: (context, snapshot) => Scaffold(
        appBar: AppBar(
          title: const Text('Post Frame Callback Stateless'),
        ),
        body: const Center(
          child: Text('Body of the Scaffold'),
        ),
      ),
    );
  }

  Future<void> _showDialog(BuildContext context) async {
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          content: const Text('Dialog content'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Accept'),
            ),
          ],
        ),
      );
    });
  }
}

Showing a dialog when you add a new widget to your tree will never be a problem again. 😀

You can find the samples of this tutorial here.

Happy coding!

Did you find this article valuable?

Support David Serrano Canales by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors