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?

Difficulty: Beginner

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

class Sample extends StatefulWidget {
  @override
  _SampleState createState() => new _SampleState();
}

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

  @override
  void initState() {
    super.initState();
    _showDialog();
  }

  _buildList(){
    if (countries.length == 0){
      return new Container();
    }

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

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

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

I was surprised to see that despite the setState in lines #34-36, 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
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

class Sample extends StatefulWidget {
  @override
  _SampleState createState() => new _SampleState();
}

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

  @override
  void initState() {
    super.initState();
    _showDialog();
  }

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

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

class MyDialogContent extends StatefulWidget {
  MyDialogContent({
    Key key,
    this.countries,
  }): super(key: key);

  final List<String> countries;

  @override
  _MyDialogContentState createState() => new _MyDialogContentState();
}

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

  @override
  void initState(){
    super.initState();
  }

  _getContent(){
    if (widget.countries.length == 0){
      return new Container();
    }

    return new Column(
      children: new List<RadioListTile<int>>.generate(
        widget.countries.length,
        (int index){
          return new RadioListTile<int>(
            value: index,
            groupValue: _selectedIndex,
            title: new Text(widget.countries[index]),
            onChanged: (int value) {
              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.