Home > Software design >  in flutter dynamic listview removing an item is removing the last instead of removing selected index
in flutter dynamic listview removing an item is removing the last instead of removing selected index

Time:10-11

I've recently asked a question on how to create a group of form dynamically. and i've got an answer. but the problem was when removed an index of the group it removes the last added form. but the value is correct. the group form consists of two text form fields and one dropdown. (code is below)

for example if i add 3 dynamic group formfields and removed the second index index[1] the ui update will remove the last index but the removed value is only the selected index. why is the ui not working as expected?

import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';

class Purchased extends StatefulWidget {
  const Purchased({Key? key}) : super(key: key);

  @override
  State<Purchased> createState() => _PurchasedState();
}

class _PurchasedState extends State<Purchased> {
  List<UserInfo> list = [];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// every time you add new Userinfo, it will generate new FORM in the UI
          list.add(UserInfo());
          setState(() {}); // dont forget to call setState to update UI
        },
        child: const Icon(Icons.add),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  return Column(
                    children: [
                      Text('phone'),
                      Text(list[index].phone),
                      Text('email'),
                      Text(list[index].email),
                      Text('category'),
                      Text(list[index].category)
                    ],
                  );
                })),
          ),
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  return MyForm(
                      // dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
                      key: ValueKey(index),
                      //pass init value so the widget always update with current value
                      initInfo: list[index],
                      // every changes here will update your current list value
                      onChangePhone: (phoneVal) {
                        if (phoneVal != null) {
                          list[index].setPhone(phoneVal);
                          setState(() {});
                        }
                      },
                      // every changes here will update your current list value
                      onchangeEmail: (emailVal) {
                        if (emailVal != null) {
                          list[index].setEmail(emailVal);
                          setState(() {});
                        }
                      },
                      onchangeCategory: (categoryVal) {
                        if (categoryVal != null) {
                          list[index].setCategory(categoryVal);
                          setState(() {});
                        }
                      },
                      onremove: () {
                        list.removeAt(index);
                        setState(() {});
                      });
                })),
          )
        ],
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  final UserInfo initInfo;
  final Function(String?) onChangePhone;
  final Function(String?) onchangeEmail;
  final Function(String?) onchangeCategory;
  final VoidCallback? onremove;
  const MyForm({
    key,
    required this.initInfo,
    required this.onChangePhone,
    required this.onchangeEmail,
    required this.onchangeCategory,
    required this.onremove,
  });

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  TextEditingController _phoneCtrl = TextEditingController();
  TextEditingController _emailCtrl = TextEditingController();
  String? selected;

  final List<String> category = [
    'Manager',
    'Reception',
    'Sales',
    'Service',
  ];

  @override
  void initState() {
    super.initState();
    // set init value
    _phoneCtrl = TextEditingController(text: widget.initInfo.phone);
    _emailCtrl = TextEditingController(text: widget.initInfo.email);
    selected = widget.initInfo.category;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
          TextFormField(
            controller: _phoneCtrl,
            onChanged: widget.onChangePhone,
          ),
          TextFormField(
            controller: _emailCtrl,
            onChanged: widget.onchangeEmail,
          ),

          DropdownButtonFormField2(
              //key: _key,
              decoration: InputDecoration(
                isDense: true,
                contentPadding: EdgeInsets.zero,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(5),
                ),
              ),
              isExpanded: true,
              hint: const Text(
                'Select Category',
                style: TextStyle(fontSize: 14),
              ),
              icon: const Icon(
                Icons.arrow_drop_down,
                color: Colors.black45,
              ),
              iconSize: 30,
              buttonHeight: 60,
              buttonPadding: const EdgeInsets.only(left: 20, right: 10),
              items: category
                  .map((item) => DropdownMenuItem<String>(
                        value: item,
                        child: Text(
                          item,
                          style: const TextStyle(
                            fontSize: 14,
                          ),
                        ),
                      ))
                  .toList(),
              validator: (value) {
                if (value == null) {
                  return 'Please select Catagory.';
                }
              },
              onChanged: widget.onchangeCategory,
              onSaved: widget.onchangeCategory)

          /// same like TextFormField, you can create new widget below
          /// for dropdown, you have to 2 required value
          /// the initValue and the onchage function
        ],
      ),
    );
  }
}

class UserInfo {
  ///define
  String _phone = '';
  String _email = '';
  String _category = '';

  /// getter
  String get phone => _phone;
  String get email => _email;
  String get category => _category;

  ///setter
  void setPhone(String phone) {
    _phone = phone;
  }

  void setEmail(String email) {
    _email = email;
  }

  void setCategory(String category) {
    _category = category;
  }
}

any help is appreciated.

new approach. worked for text field but not dropdown

import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';

class Purchased extends StatefulWidget {
  const Purchased({Key? key}) : super(key: key);

  @override
  State<Purchased> createState() => _PurchasedState();
}

class _PurchasedState extends State<Purchased> {
  List<UserInfo> list = [];
  List<TextEditingController> textControllerList = [];
  List<TextEditingController> textControllerList1 = [];
  Map<String, String> listCtrl = {};
  @override
  void dispose() {
    textControllerList.forEach((element) {
      element.dispose();
    });
    textControllerList1.forEach((element) {
      element.dispose();
    });
    listCtrl;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// every time you add new Userinfo, it will generate new FORM in the UI
          list.add(UserInfo());
          setState(() {}); // dont forget to call setState to update UI
        },
        child: const Icon(Icons.add),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  return Column(
                    children: [
                      Text('phone'),
                      Text(list[index].phone),
                      Text('email'),
                      Text(list[index].email),
                      Text('category'),
                      Text(list[index].category)
                    ],
                  );
                })),
          ),
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  TextEditingController controller = TextEditingController();
                  TextEditingController controller1 = TextEditingController();
                  textControllerList.add(controller);
                  textControllerList1.add(controller1);
                  return MyForm(
                      // dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
                      textEditingController: textControllerList[index],
                      textEditingController1: textControllerList1[index],
                      key: ValueKey(index),
                      //pass init value so the widget always update with current value
                      initInfo: list[index],
                      dataCtrl: listCtrl,
                      // every changes here will update your current list value
                      onChangePhone: (phoneVal) {
                        if (phoneVal != null) {
                          list[index].setPhone(phoneVal);
                          setState(() {});
                        }
                      },
                      // every changes here will update your current list value
                      onchangeEmail: (emailVal) {
                        if (emailVal != null) {
                          list[index].setEmail(emailVal);
                          setState(() {});
                        }
                      },
                      onchangeCategory: (categoryVal) {
                        if (categoryVal != null) {
                          list[index].setCategory(categoryVal);

                          setState(() {});
                        }
                      },
                      onremove: () {
                        list.removeAt(index);
                        textControllerList.removeAt(index);
                        textControllerList1.removeAt(index);
                        if (listCtrl.containsKey(index)) {
                          listCtrl.remove(index);
                        }
                        setState(() {});
                      });
                })),
          )
        ],
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  final UserInfo initInfo;
  final Function(String?) onChangePhone;
  final Function(String?) onchangeEmail;
  final Function(String?) onchangeCategory;
  final TextEditingController textEditingController;
  final TextEditingController textEditingController1;
  Map<String, String> dataCtrl = {};
  final VoidCallback? onremove;
  MyForm({
    key,
    required this.initInfo,
    required this.onChangePhone,
    required this.onchangeEmail,
    required this.onchangeCategory,
    required dataCtrl,
    required this.onremove,
    required this.textEditingController,
    required this.textEditingController1,
  });

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  TextEditingController _phoneCtrl = TextEditingController();
  TextEditingController _emailCtrl = TextEditingController();
  String? selected;

  final List<String> category = [
    'Manager',
    'Reception',
    'Sales',
    'Service',
  ];

  @override
  void initState() {
    super.initState();
    // set init value
    _phoneCtrl = TextEditingController(text: widget.initInfo.phone);
    _emailCtrl = TextEditingController(text: widget.initInfo.email);
    selected = widget.initInfo.category;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
          TextFormField(
            controller: widget.textEditingController,
            onChanged: widget.onChangePhone,
          ),
          TextFormField(
            controller: widget.textEditingController1,
            onChanged: widget.onchangeEmail,
          ),

          DropdownButtonFormField2(
            //key: _key,
            decoration: InputDecoration(
              isDense: true,
              contentPadding: EdgeInsets.zero,
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(5),
              ),
            ),
            isExpanded: true,
            hint: const Text(
              'Select Category',
              style: TextStyle(fontSize: 14),
            ),
            icon: const Icon(
              Icons.arrow_drop_down,
              color: Colors.black45,
            ),
            iconSize: 30,
            buttonHeight: 60,
            //value: category[1],
            value: selected!.isEmpty ? null : selected,
            buttonPadding: const EdgeInsets.only(left: 20, right: 10),
            items: category
                .map((item) => DropdownMenuItem<String>(
                      value: item,
                      child: Text(
                        item,
                        style: const TextStyle(
                          fontSize: 14,
                        ),
                      ),
                    ))
                .toList(),
            validator: (value) {
              if (value == null) {
                return 'Please select Catagory.';
              }
            },
            onChanged: widget.onchangeCategory,
            onSaved: widget.onchangeCategory,
          )

          /// same like TextFormField, you can create new widget below
          /// for dropdown, you have to 2 required value
          /// the initValue and the onchage function
        ],
      ),
    );
  }
}

class UserInfo {
  ///define
  String _phone = '';
  String _email = '';
  String _category = '';

  /// getter
  String get phone => _phone;
  String get email => _email;
  String get category => _category;

  ///setter
  void setPhone(String phone) {
    _phone = phone;
  }

  void setEmail(String email) {
    _email = email;
  }

  void setCategory(String category) {
    _category = category;
  }
}

CodePudding user response:

You are dynamically creating TextEditingControllers but have no way of keeping track of them. You need a way to keep track of all the controllers by creating a List<TextEditingController>

The reason your code is not working, other than the above, is because you are setting the text for each textEditingController in the initState() method. This only gets called once, so when the tree rebuilds it is using the 'old' value stored in the controller.

I propose the following:

  1. MyForm() should take a textEditingController as a parameter
  2. On the Purchase() class create a List<TextEditingControllers>
  3. Using the index on ListView.builder dynamically add a textController to the list each time you add a new widget.
  4. Remove the textController when the removeAt() method is called.
  5. Don't forget to dispose your textEditingControllers

Please refer to the code below. NOTE: I have only left the phonecontroller for simplicity

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Purchased(),
    );
  }
}

class Purchased extends StatefulWidget {
  const Purchased({Key? key}) : super(key: key);

  @override
  State<Purchased> createState() => _PurchasedState();
}

class _PurchasedState extends State<Purchased> {
  List<UserInfo> list = [];
  List<TextEditingController> textControllerList = [];

  @override
  void dispose() {
    textControllerList.forEach((element) {
      element.dispose();
    });
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// every time you add new Userinfo, it will generate new FORM in the UI
          list.add(UserInfo());
          setState(() {}); // dont forget to call setState to update UI
        },
        child: const Icon(Icons.add),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  return Column(
                    children: [
                      const Text('phone'),
                      Text(list[index].phone),
                    ],
                  );
                })),
          ),
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  TextEditingController controller = TextEditingController();
                  textControllerList.add(controller);
                  return MyForm(
                      // dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
                      textEditingController: textControllerList[index],
                      key: ValueKey(index),
                      //pass init value so the widget always update with current value
                      initInfo: list[index],
                      // every changes here will update your current list value
                      onChangePhone: (phoneVal) {
                        if (phoneVal != null) {
                          setState(() {
                            list[index].setPhone(phoneVal);
                          });
                        }
                      },
                      // every changes here will update your current list value

                      onremove: () {
                        list.removeAt(index);
                        textControllerList.removeAt(index);
                        setState(() {});
                      });
                })),
          )
        ],
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  final UserInfo initInfo;
  final Function(String?) onChangePhone;
  final TextEditingController textEditingController;

  final VoidCallback? onremove;
  const MyForm({
    super.key,
    required this.initInfo,
    required this.onChangePhone,
    required this.onremove,
    required this.textEditingController,
  });

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          IconButton(
              onPressed: widget.onremove,
              icon: const Icon(
                Icons.remove,
              )),
          TextFormField(
            controller: widget.textEditingController,
            onChanged: widget.onChangePhone,
          ),
        ],
      ),
    );
  }
}

class UserInfo {
  ///define
  String _phone = '';

  /// getter
  String get phone => _phone;

  ///setter
  void setPhone(String phone) {
    _phone = phone;
  }
}
  • Related