这篇文章主要受以下这篇文章的启发:
How to refresh the content of a Dialog via setState?
在上面链接中的这篇文章中,主要介绍了在Flutter中使用setState刷新Dialog的问题,并提供了一种解决方案。这篇文章大部分内容翻译自这个链接,另外除了坐着提出的方案之外,另外再介绍一种更简单实用的解决方案。
学习到的内容:
- 如何在Flutter的Widget中弹出
Dialog
- 如何正确使用setState来刷新Dialog的显示内容
- 如何使用
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
组件
运行之后,显示效果如下:
接下来重点看一下_buildList
这段代码:
从图中的指示可以看出,在创建的Dialog中,有一个RadioList控件,每当点击RadioList中的某一item时,调用setState方法重新设置被选中item的下标位置,并刷新UI。但是在运行上述代码之后,我们点击Dialog中的所有item都没有任何反应。
问题原因
我们用Android Studio中的Flutter inspector来查看一下当前Flutter布局视图如下:
可以看出DialogSample和我们所创建的Dialog是平级关系,并不是从属关系。问题的原因会不会跟这个有关系呢?
然后我在网上又进行了一番查找,问题的原因终于找到了:
setState
方法的作用对象是它所指向的一个StatefulWidget,在这个例子中,setState
并不是作用于Dialog,而是DialogSample这个组件。
虽然我们是在Dialog内部的RadioList中调用了setState
方法,但是此setState方法实际操作对象是DialogSample,如下图所以:
解决方案
1 为Dialog单独设置State
我们需要重构一下showDialog方法,具体修改如下:
可以看出,我们不在调用 _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,
);
}