I want to update my ListView if i remove or add items. Right now i just want to delete items and see the deletion of the items immediately.
My application is more complex so i wrote a small example project to show my problems.
The TestItem
class holds some data entries:
class TestItem {
static int id = 1;
bool isFinished = false;
String text;
TestItem() {
text = "Item ${id++}";
}
}
The ItemInfoViewWidget
is the UI representation of the TestItem
and removes the item if it is finished (whenever the Checkbox is changed to true).
class ItemInfoViewWidget extends StatefulWidget {
TestItem item;
List<TestItem> items;
ItemInfoViewWidget(this.items, this.item);
@override
_ItemInfoViewWidgetState createState() =>
_ItemInfoViewWidgetState(this.items, this.item);
}
class _ItemInfoViewWidgetState extends State<ItemInfoViewWidget> {
TestItem item;
List<TestItem> items;
_ItemInfoViewWidgetState(this.items, this.item);
@override
Widget build(BuildContext context) {
return new Card(
child: new Column(
children: <Widget>[
new Text(this.item.text),
new Checkbox(
value: this.item.isFinished, onChanged: isFinishedChanged)
],
),
);
}
void isFinishedChanged(bool value) {
setState(() {
this.item.isFinished = value;
this.items.remove(this.item);
});
}
}
The ItemViewWidget
class builds the ListView
.
class ItemViewWidget extends StatefulWidget {
List<TestItem> items;
ItemViewWidget(this.items);
@override
_ItemViewWidgetState createState() => _ItemViewWidgetState(this.items);
}
class _ItemViewWidgetState extends State<ItemViewWidget> {
List<TestItem> items;
_ItemViewWidgetState(this.items);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text('Test'),
),
body: ListView.builder(
itemCount: this.items.length,
itemBuilder: (BuildContext context, int index) {
return new ItemInfoViewWidget(this.items, this.items[index]);
}),
);
}
}
The MyApp
shows one TestItem
and a button that navigates to the ItemViewWidget
page.
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List<TestItem> items = new List<TestItem>();
_MyHomePageState() {
for (int i = 0; i < 20; i++) {
this.items.add(new TestItem());
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Column(
children: <Widget>[
ItemInfoViewWidget(this.items, this.items.first),
FlatButton(
child: new Text('Open Detailed View'),
onPressed: buttonClicked,
)
],
));
}
void buttonClicked() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ItemViewWidget(this.items)),
);
}
}
If i toggle the Checkbox of the first item, the Checkbox is marked as finished (as expected), but it is not removed from the UI - however it is removed from the list.
Then I go back to the Main page and I can observe that Item 1 is checked there as well.
So if I go to the ItemViewWidget
page again, I can observe that the checked items are no longer present.
Based on these observations, I come to the conclusion that my implementation works, but my UI is not updating.
How can I change my code to make an immediate update of the UI possible?
Edit: This is not a duplicate, because
this.items = List.from(this.items);
but the behavior of my app is the same as already described above.List.from
, because my architecture has one list that is referenced by several classes. If i break the chain i have to update all references by my own. Is there a problem with my architecture? The real problem lies in the fact that it is your list item that removes delete an element from your list. The thing is since it's your item which does the computing, the parent is never notified that the list changed. Therefore it doesn't know it should rerender. So nothing visually change.
Your widget state only holds the list of Strings. When you remove one of them and the widget is rebuilt, Flutter knows that your ListView contains one less child widget, so it removes the last one and rebuilds the other ones.
To remove the elements from the tree and then put them back in, attach a key to the ListView as commented by Marcin Szałek. For stateful widgets, elements will be updated when the corresponding new widget is of the same widget type and has the same key.
The reason for removing not updating the deleted element is the second widget is stateful widget it needs force rendering in some case use this code to get reload the view by overriding the didUpdateWidget method
I dont want to create a new instance of my list just to get the UI updated.
Flutter uses immutable object. Not following this rule is going against the reactive framework. It is a voluntary requirement to reduce bugs.
Fact is, this immutability is here especially to prevents developers from doing what you currently do: Having a program that depends on sharing the same instance of an object between classes; as multiple classes may want to modify it.
The real problem lies in the fact that it is your list item that removes delete an element from your list.
The thing is since it's your item which does the computing, the parent is never notified that the list changed. Therefore it doesn't know it should rerender. So nothing visually change.
To fix that you should move the deletion logic to the parent. And make sure that the parent correctly calls setState
accordingly. This would translate into passing a callback to your list item, which will be called on deletion.
Here's an example:
class MyList extends StatefulWidget {
@override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<String> list = List.generate(100, (i) => i.toString());
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return MyItem(list[index], onDelete: () => removeItem(index));
},
);
}
void removeItem(int index) {
setState(() {
list = List.from(list)
..removeAt(index);
});
}
}
class MyItem extends StatelessWidget {
final String title;
final VoidCallback onDelete;
MyItem(this.title, {this.onDelete});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(this.title),
onTap: this.onDelete,
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With