我正在尝试构建一个页面,用户可以在其中添加和删除列表中的元素。 我正在为一个问题而苦苦挣扎。当我删除元素时,会在更新UI的同时正确更新模型,但是方式错误:仅删除列表的最后一个元素。
我正在使用Flutter 1.5.4。
我已经为列表使用了更简单的元素,并且我尝试仅使用此页面来构建一个新项目以消除所有可能的问题,但是仍然无法正常工作。
我也尝试使用Column而不是List,但结果始终相同。
main.dart:
import 'package:flutter/material.dart';
import './widget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Hello'),
),
body: Center(
child: SectionWidget(),
),
),
);
}
}
widget.dart:
import 'package:flutter/material.dart';
class SectionWidget extends StatefulWidget {
_SectionWidgetState createState() => new _SectionWidgetState();
}
class _SectionWidgetState extends State<SectionWidget> {
List<String> _items = List<String>();
@override
Widget build(BuildContext context) {
List<Widget> children = [
ListTile(
title: Text(
"Strings",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => setState(() {
_items.add('item ${_items.length}');
print("Adding "+ _items.last);
}),
),
),
];
children.addAll(_buildForms());
return ListView(
children: children,
);
}
List<Widget> _buildForms() {
List<Widget> forms = new List<Widget>();
print("Strings:" + _items.toString());
for (String item in _items) {
forms.add(
ListTile(
leading: Icon(Icons.person),
title: TextFormField(
initialValue: item,
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => setState(() {
print("Removing $item");
_items.remove(item);
}),
),
),
);
}
return forms;
}
}
如果我将4个项目添加到列表中,然后删除“项目1”,则这是控制台上的输出:
I/flutter ( 4192): Strings:[]
I/flutter ( 4192): Adding item 0
I/flutter ( 4192): Strings:[item 0]
I/flutter ( 4192): Adding item 1
I/flutter ( 4192): Strings:[item 0, item 1]
I/flutter ( 4192): Adding item 2
I/flutter ( 4192): Strings:[item 0, item 1, item 2]
I/flutter ( 4192): Adding item 3
I/flutter ( 4192): Strings:[item 0, item 1, item 2, item 3]
I/flutter ( 4192): Removing item 1
I/flutter ( 4192): Strings:[item 0, item 2, item 3]
这是正确的,因为从列表中删除了“项目1”,但是如果我看一下UI,这就是我得到的:
列表中的元素数量正确,模型正确,但是显示的元素错误。
该如何解决?我是在做错什么,还是Flutter的错误?
答案 0 :(得分:13)
您可以做的是传递给ListView
和Key
,其中将包含列表中元素的长度。
ListView(
key: Key(myList.length.toString()),
children: myList.map(...),
),
为什么有效?
它会导致整个ListView
从头开始重建自身,但前提是列表的长度发生变化。重要的是,在其他情况下(例如在TextField中键入内容)不会发生这种情况,因为这种操作会使您失去对每种字母类型的关注。
希望它会有所帮助;)
答案 1 :(得分:1)
这不是解决方案,而是一种解释,有关解决方案,请参阅MarcinSzałek的评论。
ListView
未按预期方式重新呈现的原因是TextFormField
是一个有状态的小部件。重建小部件时,需要注意引擎盖下有3棵树,分别是Widget,Element和RenderObject树。 RenderObject是视觉表示,并且直接基于Element。元素直接基于Widget,而Widget只是配置,即您在代码中键入的内容。
首次实例化窗口小部件时,将创建一个对应的元素。但是,当您调用setState()
来更改_items列表时,将重新构建窗口小部件,但不会破坏和重新创建该元素。相反,为了最大化效率,Flutter的设计方法是首先比较初始ListTile
小部件和重建ListTile
小部件之间的差异。在这种情况下,最初有4个图块,然后有3个图块。更新第二个和第三个图块的元素以具有重建ListTile
小部件的属性,而不是重新创建新的图块(并且第一个图块没有更改,因为之前和之后的窗口小部件/配置相同) 。从“元素”树中弹出第四个元素。 Flutter官方团队的video在解释这三棵树方面做得很好。
使用Dart的Devtools,您不会发现第三个元素的键/ ID没有变化。当您检查第三个ListTile
时,密钥是#3dded 之前和之后。
初始
删除后
要从树中删除元素然后放回去,请按MarcinSzałek的评论在ListView
上附加一个键。对于有状态的小部件,当相应的新小部件具有相同的小部件类型并具有相同的键时,将更新元素。在没有显式指定键的情况下,该元素将仅检查其类型是否与相应的小部件相同,并且由于所有小部件的运行时类型均为ListTile
,因此不会对元素进行任何更新,因此您会看到项目0和项目1而不是项目0和项目2。我建议您阅读有关键here的更多信息。
使用Dart的Devtools,您可以在第三个ListTile
的键中找到更改。最初,当它是项目1时,键是#5addd 。然后,当删除项目1时,密钥将更改为#26044 。
初始
删除后
答案 2 :(得分:0)
我认为,您需要为每个文本字段创建控制器或使用hintText
Scaffold(
appBar: AppBar(
title: Text('Hello'),
),
body: ListView(
children: <Widget>[
ListTile(
title: Text(
'Strings',
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => setState(() {
_items.add('item ${_items.length}');
print('Adding '+ _items.last);
}),
),
),
ListView.builder(
physics: BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: _items.length,
itemBuilder: (BuildContext context, int i){
return ListTile(
leading: Icon(Icons.person),
title: TextFormField(
decoration: InputDecoration(
hintText: _items[i]
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => setState(() {
print('Removing ${_items[i]}');
_items.remove(_items[i]);
}),
),
);
},
)
]
)
);
答案 3 :(得分:0)
您的窗口小部件状态仅包含字符串列表。当您删除其中一个并重新构建窗口小部件时,Flutter知道您的ListView
少包含一个子窗口小部件,因此它将删除最后一个窗口小部件并重建其他窗口小部件。
这里的要点是您使用initialValue
来设置TextFormFields
上的文本-initialValue
的要点是它不会更改字段上的文本即使您更改它-TextFormField
只能有1个初始值,这就是您最初设置的值。
如果要更改文本,则需要使用TextEditingController
。在上面的示例中,只需将initialValue: item
替换为controller: TextEditingController(text: item)
就可以解决。但是,在实际的应用程序中,您可能希望将控制器存储在小部件的状态下,以便用户输入的值在整个重建过程中得以持久保存。
答案 4 :(得分:0)
据我了解,setState
函数仅更新窗口小部件数据,而不重新创建窗口小部件。在代码中,您使用initialValue为TextField
设置默认文本。仅在创建TextField
时使用此参数。因此,当您删除一项时,应用程序将根据_item
的长度来更新小部件(在这种情况下,请保留前三个,然后删除最后一个)。 initialValue
目前没有意义,因为TextField
未被创建而是被重用。
要解决此问题,您可以使用TextEditingController而不是initialVal
设置文本
首先,使用TextEditingController
创建_items
地图的列表
List<TextEditingController> controllers = [];
然后,在添加项目时也为其创建一个新的控制器
onPressed: () => setState(() {
_items.add('item ${_items.length}');
controllers.add(TextEditingController(text: "${_items.length}"));
print("Adding "+ _items.last);
}),
最后,将控制器添加到TextField
TextFormField(
controller: controllers[_items.indexOf(item)],
)
啊,别忘了先删除控制器再删除项目
controllers.removeAt(_items.indexOf(item));
希望有帮助。
答案 5 :(得分:0)
删除不更新已删除元素的原因是第二个小部件是有状态的小部件,在某些情况下需要强制渲染
使用此代码通过覆盖didUpdateWidget方法来重新加载视图
@override
void didUpdateWidget(LoadImageFromPathAsync oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget != widget) {
setState(() {});
}
}
答案 6 :(得分:0)
将键或控制器字段用于TextFormField元素。
TextFormField(
key: GlobalKey(),
....
)