matte_colo
matte_colo

Reputation: 355

Flutter - UI is not updated correctly when removing an element

I'm trying to build a page in which users can add and remove elements from a list. I'm struggling with a problem. When I remove an element, the model is updated correctly while the UI is updated but in a wrong way: only the last element of the list is removed.

I'm using Flutter 1.5.4.

I already using simpler elements for the list and I tried to build a new project with only this page to remove all possible problems, but it still doesn't work correctly.

I also tried using a Column instead of a List but the result is always the same.

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;
  }
}

If I add 4 items to the list and then remove "item 1", this is the output on the console:

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]

Which is correct, as "item 1" was removed from the list, but if I look at the UI, this is what i get:

enter image description here

The number of elements in the list is correct, the model is correct, but the elements shown are wrong.

How do I fix this? Am I doing something wrong or is it a bug of Flutter?

Upvotes: 14

Views: 8114

Answers (8)

Abdulmalek Dery
Abdulmalek Dery

Reputation: 1016

I tried Marcin Szałek Solution but I'm using BLoC to rebuild the widget so his solution caused an issue as follow:

1- add first element (TextField) to the list and type "a" in it

2- add second element (TextField) to the list (boom the first TextField will become empty)

I don't actually know if this will happen when we are not using the BLoC. But if you are using BLoC try this solution it worked for me like a charm:

1- create list of UniqueKey

List<UniqueKey> listKeys = [];

2- when you click on the add icon to add an element to the ListView make sure to add object of UniqueKey to the listKeys

listKeys.add(UniqueKey());

3- attach each listKeys list members to the ListView items

ListView.builder(
   shrinkWrap: true,
   itemCount: items?.length ?? 0,
   itemBuilder: (context, index) {
         return ListItem(
            key: listKeys[index];
      );
    },
   )

4- when you remove element "TextField" form the ListView you must delete it form the listKyes to

listKeys.removeAt(deleteIndex);

Upvotes: 2

Tox
Tox

Reputation: 194

This is not a solution but an explanation, refer to Marcin Szałek's comment for the solution.

The reason that the ListView does not rerender as expected is that the TextFormField is a stateful widget. When you rebuild the widgets, you need to take note that there are 3 trees under the hood, which are the Widget, Element and RenderObject tree. The RenderObject is the visual representation, and is directly based on the Element. Whereas the Element is directly based on the Widget, and the Widget is just the configuration, i.e. what you type in code.

When first instantiating a widget, a corresponding element is created. However, when you call setState() to change the _items list, the widget is rebuild, but the element is not destroyed and recreated. Rather, in order to maximise efficiency, Flutter is made such that it first compares any differences between the initial ListTile widgets and the rebuild ListTile widgets. In this case, initially there are 4 tiles, and afterwards there are 3 tiles. Instead of recreating new tiles, the elements of the second and third tiles are updated to have the attributes of the rebuild ListTile widgets (and no changes occur for the first tile because the widget/configuration before and after is the same). The 4th element is popped from the Element tree. This video by the official Flutter team does a great job at explaining the three trees.

Using Dart's Devtools, you can find no change in the key/id of the third element. When you inspect the third ListTile, before and after the key is #3dded.


Initial

Phone Initially

After deletion

Phone Finally

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. Without explicitly specifying the key, the element will only check whether it is of the same type as the corresponding widget, and as all the widgets are of runtime type ListTile, then there would not be any update of the elements, thus you see item 0 and item 1 instead of item 0 and item 2. I recommend that you read more about keys here.

Using Dart's Devtools, you can find the change in the key of the third ListTile. Initially, when it was item 1, the key was #5addd. Then, when item 1 is deleted, the key changes to #26044.


Initial

Phone Initially

After deletion

Phone Finally

Upvotes: 7

Marcin Szałek
Marcin Szałek

Reputation: 5069

What you can do is pass to the ListView the Key which will contain the length of the elements in the list.

ListView(
  key: Key(myList.length.toString()),
  children: myList.map(...),
),

Why it works?
It causes the whole ListView to rebuild itself from scratch but only when the length on your list changes. What's important is that it doesn't happen in other cases (like typing inside a TextField) because such action would cause you to lose focus on every letter type.

Hope it helps ;)

Upvotes: 32

kaya
kaya

Reputation: 754

Use key or controller fields for TextFormField elements.

TextFormField(
    key: GlobalKey(),
    ....
) 

Upvotes: 0

Aravindh Kumar
Aravindh Kumar

Reputation: 1243

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

 @override
  void didUpdateWidget(LoadImageFromPathAsync oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget != widget) {
        setState(() {});
    }
  }

Upvotes: 2

As I understand, setState function only update widget data not re-create widget. In your code, you use initialValue to set default text for TextField. This param is only used when TextField is created. So when you remove an item, app will update widget depends on _item's length (in this case, keep 3 first one and remove the last one). initialValue is no meaning at this time because TextField is not created but reused.

To solve this, you can use TextEditingController to set text instead of initialVal

First, create a list of TextEditingController map with _items

  List<TextEditingController> controllers = [];

Then, when add an item also create a new controller for it

onPressed: () => setState(() {
  _items.add('item ${_items.length}');
  controllers.add(TextEditingController(text:  "${_items.length}"));
  print("Adding "+ _items.last);
}),

Finally, add controller to TextField

TextFormField(
  controller: controllers[_items.indexOf(item)],
)

Ah, don't forget to remove controller before remove item

controllers.removeAt(_items.indexOf(item));

Hope it helps.

Upvotes: 0

Ovidiu
Ovidiu

Reputation: 8712

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.

The catch here is that you're using initialValue to set the text on the TextFormFields - the whole point of initialValue is that it won't change the text on the field even if you change it - a TextFormField can only have 1 initial value and that's the one you're setting initially.

If you want the text to change, you need to use a TextEditingController. In your example above, simply replacing initialValue: item with controller: TextEditingController(text: item) should fix it. In a real application you'd likely want to store the controllers in the state of the widget though, so that the user-entered values are persisted across rebuilds.

Upvotes: 0

Arthur Khabirov
Arthur Khabirov

Reputation: 146

I think, that you need to create controllers for every textfield or use 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]);
                        }),
                  ),
                );
              },
            )
          ]
        )
    );

Upvotes: 0

Related Questions