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:
1
2import 'package:flutter/cupertino.dart';
3import 'package:flutter/material.dart';
4
5class Sample extends StatefulWidget {
6 const Sample({super.key});
7
8
9 State<Sample> createState() => _SampleState();
10}
11
12class _SampleState extends State<Sample> {
13 final List<String> countries = <String>[
14 'Belgium',
15 'France',
16 'Italy',
17 'Germany',
18 'Spain',
19 'Portugal'
20 ];
21 int _selectedCountryIndex = 0;
22
23
24 void initState() {
25 super.initState();
26 WidgetsBinding.instance.addPostFrameCallback((_) {
27 _showDialog();
28 });
29 }
30
31 _buildList() {
32 if (countries.isEmpty) {
33 return const SizedBox.shrink();
34 }
35
36 return Column(
37 children:
38 List<RadioListTile<int>>.generate(countries.length, (int index) {
39 return RadioListTile<int>(
40 value: index,
41 groupValue: _selectedCountryIndex,
42 title: Text(countries[index]),
43 onChanged: (int? value) {
44 if (mounted) {
45 setState(() {
46 _selectedCountryIndex = value!;
47 });
48 }
49 },
50 );
51 }));
52 }
53
54 _showDialog() async {
55 await showDialog<String>(
56 context: context,
57 builder: (BuildContext context) {
58 return CupertinoAlertDialog(
59 title: const Text('Please select'),
60 actions: <Widget>[
61 CupertinoDialogAction(
62 isDestructiveAction: true,
63 onPressed: () {
64 Navigator.of(context).pop('Cancel');
65 },
66 child: const Text('Cancel'),
67 ),
68 CupertinoDialogAction(
69 isDestructiveAction: true,
70 onPressed: () {
71 Navigator.of(context).pop('Accept');
72 },
73 child: const Text('Accept'),
74 ),
75 ],
76 content: SingleChildScrollView(
77 child: Material(
78 child: _buildList(),
79 ),
80 ),
81 );
82 },
83 barrierDismissible: false,
84 );
85 }
86
87
88 Widget build(BuildContext context) {
89 return Container();
90 }
91}
92
93
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.
1
2import 'package:flutter/cupertino.dart';
3import 'package:flutter/material.dart';
4
5class Sample extends StatefulWidget {
6 const Sample({super.key});
7
8
9 State<Sample> createState() => _SampleState();
10}
11
12class _SampleState extends State<Sample> {
13 final List<String> countries = <String>[
14 'Belgium',
15 'France',
16 'Italy',
17 'Germany',
18 'Spain',
19 'Portugal'
20 ];
21
22
23 void initState() {
24 super.initState();
25 WidgetsBinding.instance.addPostFrameCallback((_) {
26 _showDialog();
27 });
28 }
29
30 _showDialog() async {
31 await showDialog<String>(
32 context: context,
33 builder: (BuildContext context) {
34 return CupertinoAlertDialog(
35 title: const Text('Please select'),
36 actions: <Widget>[
37 CupertinoDialogAction(
38 isDestructiveAction: true,
39 onPressed: () {
40 Navigator.of(context).pop('Cancel');
41 },
42 child: const Text('Cancel'),
43 ),
44 CupertinoDialogAction(
45 isDestructiveAction: true,
46 onPressed: () {
47 Navigator.of(context).pop('Accept');
48 },
49 child: const Text('Accept'),
50 ),
51 ],
52 content: SingleChildScrollView(
53 child: Material(
54 child: MyDialogContent(countries: countries),
55 ),
56 ),
57 );
58 },
59 barrierDismissible: false,
60 );
61 }
62
63
64 Widget build(BuildContext context) {
65 return Container();
66 }
67}
68
69class MyDialogContent extends StatefulWidget {
70 const MyDialogContent({
71 super.key,
72 required this.countries,
73 });
74
75 final List<String> countries;
76
77
78 State<MyDialogContent> createState() => _MyDialogContentState();
79}
80
81class _MyDialogContentState extends State<MyDialogContent> {
82 int _selectedIndex = 0;
83
84 Widget _getContent() {
85 if (widget.countries.isEmpty) {
86 return const SizedBox.shrink();
87 }
88
89 return Column(
90 children: List<RadioListTile<int>>.generate(
91 widget.countries.length,
92 (int index) {
93 return RadioListTile<int>(
94 value: index,
95 groupValue: _selectedIndex,
96 title: Text(widget.countries[index]),
97 onChanged: (int? value) {
98 if (mounted) {
99 setState(() {
100 _selectedIndex = value!;
101 });
102 }
103 },
104 );
105 },
106 ),
107 );
108 }
109
110
111 Widget build(BuildContext context) {
112 return _getContent();
113 }
114}
115
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.