Back to the technical archive

Archive article

Reviewed 2023-03-30

How to refresh the content of a Dialog via setState?

Suppose you have a Dialog with some Widgets such as RadioListTile, DropdowButton... or anything that might need to be updated WHILE the dialog remains visible, how to do it?

How to refresh the content of a Dialog via setState?

Background

Lately I had to display a Dialog to let the user select an item from a list and I wanted to display a list of RadioListTile.

I had no problem to show the Dialog and display the list, via the following source code:



import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class Sample extends StatefulWidget {
  const Sample({super.key});

  @override
  State<Sample> createState() => _SampleState();
}

class _SampleState extends State<Sample> {
  final List<String> countries = <String>[
    'Belgium',
    'France',
    'Italy',
    'Germany',
    'Spain',
    'Portugal'
  ];
  int _selectedCountryIndex = 0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _showDialog();
    });
  }

  _buildList() {
    if (countries.isEmpty) {
      return const SizedBox.shrink();
    }

    return Column(
        children:
            List<RadioListTile<int>>.generate(countries.length, (int index) {
      return RadioListTile<int>(
        value: index,
        groupValue: _selectedCountryIndex,
        title: Text(countries[index]),
        onChanged: (int? value) {
          if (mounted) {
            setState(() {
              _selectedCountryIndex = value!;
            });
          }
        },
      );
    }));
  }

  _showDialog() async {
    await showDialog<String>(
      context: context,
      builder: (BuildContext context) {
        return CupertinoAlertDialog(
          title: const Text('Please select'),
          actions: <Widget>[
            CupertinoDialogAction(
              isDestructiveAction: true,
              onPressed: () {
                Navigator.of(context).pop('Cancel');
              },
              child: const Text('Cancel'),
            ),
            CupertinoDialogAction(
              isDestructiveAction: true,
              onPressed: () {
                Navigator.of(context).pop('Accept');
              },
              child: const Text('Accept'),
            ),
          ],
          content: SingleChildScrollView(
            child: Material(
              child: _buildList(),
            ),
          ),
        );
      },
      barrierDismissible: false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}


I was surprised to see that despite the setState in lines #44-48, the selected RadioListTile was not refreshed when the user tapped one of the items.

Explanation

After some investigation, I realized that the setState() refers to the stateful widget in which the setState is invoked. In this example, any call to the setState() rebuilds the view of the Sample Widget, and not the one of the content of the dialog. Therefore, how to do?

Solution

A very simple solution is to create another stateful widget that renders the content of the dialog. Then, any invocation of the setState will rebuild the content of the dialog.



import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class Sample extends StatefulWidget {
  const Sample({super.key});

  @override
  State<Sample> createState() => _SampleState();
}

class _SampleState extends State<Sample> {
  final List<String> countries = <String>[
    'Belgium',
    'France',
    'Italy',
    'Germany',
    'Spain',
    'Portugal'
  ];

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _showDialog();
    });
  }

  _showDialog() async {
    await showDialog<String>(
      context: context,
      builder: (BuildContext context) {
        return CupertinoAlertDialog(
          title: const Text('Please select'),
          actions: <Widget>[
            CupertinoDialogAction(
              isDestructiveAction: true,
              onPressed: () {
                Navigator.of(context).pop('Cancel');
              },
              child: const Text('Cancel'),
            ),
            CupertinoDialogAction(
              isDestructiveAction: true,
              onPressed: () {
                Navigator.of(context).pop('Accept');
              },
              child: const Text('Accept'),
            ),
          ],
          content: SingleChildScrollView(
            child: Material(
              child: MyDialogContent(countries: countries),
            ),
          ),
        );
      },
      barrierDismissible: false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class MyDialogContent extends StatefulWidget {
  const MyDialogContent({
    super.key,
    required this.countries,
  });

  final List<String> countries;

  @override
  State<MyDialogContent> createState() => _MyDialogContentState();
}

class _MyDialogContentState extends State<MyDialogContent> {
  int _selectedIndex = 0;

  Widget _getContent() {
    if (widget.countries.isEmpty) {
      return const SizedBox.shrink();
    }

    return Column(
      children: List<RadioListTile<int>>.generate(
        widget.countries.length,
        (int index) {
          return RadioListTile<int>(
            value: index,
            groupValue: _selectedIndex,
            title: Text(widget.countries[index]),
            onChanged: (int? value) {
              if (mounted) {
                setState(() {
                  _selectedIndex = value!;
                });
              }
            },
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return _getContent();
  }
}

Conclusion

Sometimes some basic notions are tricky and setState is one of them. As the official documentation does not yet explain this, I wanted to share this with you.

Stay tuned for other hints and happy coding.