这篇文章主要受以下这篇文章的启发:
How to refresh the content of a Dialog via setState?

在上面链接中的这篇文章中,主要介绍了在Flutter中使用setState刷新Dialog的问题,并提供了一种解决方案。这篇文章大部分内容翻译自这个链接,另外除了坐着提出的方案之外,另外再介绍一种更简单实用的解决方案。

学习到的内容:

  1. 如何在Flutter的Widget中弹出Dialog
  2. 如何正确使用setState来刷新Dialog的显示内容
  3. 如何使用StateBuilder

显示Dialog

在Flutter的Widget中,可以直接调用showDialog方法来显示一个对话框。这个方法声明在Flutter SDK中的dialog.dart里。具体使用方式如下:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      debugShowCheckedModeBanner: false,
      home: DialogSample(),
    );
  }
}

class DialogSample extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new DialogSampleState();
  }
}

class DialogSampleState extends State<DialogSample> {
  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.length == 0){
      return new Container();
    }
    return new Column(
        children: new List<RadioListTile<int>>.generate(
            countries.length,
                (int index) => 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();
  }
}

稍微解释一下上面的代码:

1.首先执行runApp(MyApp), 显示DialogSample这个Widget
2. 在DialogSample中createState方法中返回DialogSampleState,通过DialogSampleState来创建显示的Widget
3. 然后在DialogSampleState的initState方法中,调用showDialog方法弹出一个对话框。对话框的title为’Please Select’,并且分别有一个Cancel按钮和Accept按钮
4. countries代表在Dialog中显示的列表数据内容
5. 最后在State的build方法中返回一个空的Container组件

运行之后,显示效果如下:



flutter dio response类型转换 flutter dialog setstate_Text


接下来重点看一下_buildList这段代码:

flutter dio response类型转换 flutter dialog setstate_List_02


从图中的指示可以看出,在创建的Dialog中,有一个RadioList控件,每当点击RadioList中的某一item时,调用setState方法重新设置被选中item的下标位置,并刷新UI。但是在运行上述代码之后,我们点击Dialog中的所有item都没有任何反应

问题原因

我们用Android Studio中的Flutter inspector来查看一下当前Flutter布局视图如下:

flutter dio response类型转换 flutter dialog setstate_Flutter_03


可以看出DialogSample和我们所创建的Dialog是平级关系,并不是从属关系。问题的原因会不会跟这个有关系呢?

然后我在网上又进行了一番查找,问题的原因终于找到了:

setState 方法的作用对象是它所指向的一个StatefulWidget,在这个例子中,setState并不是作用于Dialog,而是DialogSample这个组件。

虽然我们是在Dialog内部的RadioList中调用了setState方法,但是此setState方法实际操作对象是DialogSample,如下图所以:

flutter dio response类型转换 flutter dialog setstate_setState_04

解决方案

1 为Dialog单独设置State

我们需要重构一下showDialog方法,具体修改如下:

flutter dio response类型转换 flutter dialog setstate_ide_05


可以看出,我们不在调用 _buildList() 方法来创建一个系统List,取而代之的是创建一个自定义的 MyDialogList 组件,这个组件的代码如下:

class MyDialogList extends StatefulWidget {
  final List<String> countries;

  MyDialogList({
    Key key,
    this.countries,
  }): super(key: key);

  @override
  State<StatefulWidget> createState() {
    return new MyDialogListState();
  }

}

class MyDialogListState extends State<MyDialogList> {
  int _selectedIndex = 0;

  _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();
  }
}

在这个组件内部调用 setState 方法,则只会更新此组件自身的UI。

2 将Dialog包装在 StateBuilder中

除了创建自定义DialogList之外,我们还可以将弹出的 CupertinoAlertDialog 组件封装在一个 StateBuilder 内部,在StateBuild内部调用setState方法,同样只会重新调用StateBuilder内部的build方法来刷新UI。具体代码如下:

_showDialogWithStateBuilder() async {
    await showDialog<String>(
      context: context,
      builder: (BuildContext context){
        return StatefulBuilder(
            builder: (context, setState) {
              return new CupertinoAlertDialog(
                title: new Text('Please select'),
                actions: <Widget>[
                  new CupertinoDialogAction(
                    isDestructiveAction: true,
                    onPressed: (){Navigator.of(context).pop('cancel');},
                    child: new Text('cancel $_selectedCountryIndex'),
                  ),
                  new CupertinoDialogAction(
                    isDestructiveAction: true,
                    onPressed: (){Navigator.of(context).pop('Accept');},
                    child: new Text('Accept $_selectedCountryIndex'),
                  ),
                ],
                content: new SingleChildScrollView(
                  child: new Material(
                      child: countries.length == 0 ? Container() :
                      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;
                                    });
                                  },
                                );
                              }
                          )
                      )
                  ),
                ),
              );
            }
        );
      },
      barrierDismissible: false,
    );
  }