You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter动态ListView删除选中项异常:误删最后一项求助

问题根源分析
  1. 索引作为ValueKey的隐患:用列表索引作为ValueKey时,删除中间元素后,后续元素的索引会前移,Flutter会复用之前的Widget实例,导致旧的状态(如下拉框选中值)被错误绑定到新的数据项上。
  2. initState仅初始化一次:MyForm的initState仅在Widget创建时执行,后续父组件更新initInfo时,子组件的控制器和下拉框选中状态不会同步更新。
  3. 控制器管理错误:新尝试代码中在ListView.builder的itemBuilder里每次创建并添加控制器到列表,build方法会多次执行,导致控制器列表冗余、状态混乱。
解决方案

1. 给数据模型添加唯一标识

修改UserInfo类,添加唯一id字段,确保每个数据项有独立身份:

class UserInfo {
  final String id; // 新增唯一标识
  String _phone = '';
  String _email = '';
  String _category = '';

  UserInfo() : id = UniqueKey().toString(); // 初始化时生成唯一id

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

  void setPhone(String phone) => _phone = phone;
  void setEmail(String email) => _email = email;
  void setCategory(String category) => _category = category;
}

2. 用数据项的唯一id作为ValueKey

在ListView.builder中,将ValueKey(index)改为ValueKey(list[index].id),确保每个MyFormWidget对应唯一数据项:

return MyForm(
  key: ValueKey(list[index].id), // 改用数据项的唯一id
  // ...其他参数
);

3. 同步更新子组件状态

在MyForm的State类中,重写didUpdateWidget方法,当父组件传递的initInfo变化时,同步更新控制器和下拉框选中值:

@override
void didUpdateWidget(covariant MyForm oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 当数据模型变化时,同步更新控制器和选中值
  if (oldWidget.initInfo != widget.initInfo) {
    widget.textEditingController.text = widget.initInfo.phone;
    widget.textEditingController1.text = widget.initInfo.email;
    setState(() {
      selected = widget.initInfo.category;
    });
  }
}

4. 修正控制器管理逻辑

将控制器的创建和管理绑定到UserInfo的添加动作,避免在build中重复创建:

// 在PurchasedState的浮动按钮点击事件中
floatingActionButton: FloatingActionButton(
  onPressed: () {
    final userInfo = UserInfo();
    list.add(userInfo);
    textControllerList.add(TextEditingController(text: userInfo.phone));
    textControllerList1.add(TextEditingController(text: userInfo.email));
    setState(() {});
  },
  child: const Icon(Icons.add),
),
修改后的完整代码
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 = [];

  @override
  void dispose() {
    for (var controller in textControllerList) {
      controller.dispose();
    }
    for (var controller in textControllerList1) {
      controller.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final userInfo = UserInfo();
          list.add(userInfo);
          textControllerList.add(TextEditingController(text: userInfo.phone));
          textControllerList1.add(TextEditingController(text: userInfo.email));
          setState(() {});
        },
        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),
                    const Text('email'),
                    Text(list[index].email),
                    const Text('category'),
                    Text(list[index].category)
                  ],
                );
              }),
            ),
          ),
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: list.length,
              itemBuilder: ((context, index) {
                return MyForm(
                  key: ValueKey(list[index].id),
                  textEditingController: textControllerList[index],
                  textEditingController1: textControllerList1[index],
                  initInfo: list[index],
                  onChangePhone: (phoneVal) {
                    if (phoneVal != null) {
                      list[index].setPhone(phoneVal);
                      setState(() {});
                    }
                  },
                  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).dispose();
                    textControllerList1.removeAt(index).dispose();
                    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;
  final VoidCallback? onremove;

  const MyForm({
    super.key,
    required this.initInfo,
    required this.onChangePhone,
    required this.onchangeEmail,
    required this.onchangeCategory,
    required this.textEditingController,
    required this.textEditingController1,
    required this.onremove,
  });

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

class _MyFormState extends State<MyForm> {
  String? selected;

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

  @override
  void initState() {
    super.initState();
    selected = widget.initInfo.category;
    // 初始化控制器文本
    widget.textEditingController.text = widget.initInfo.phone;
    widget.textEditingController1.text = widget.initInfo.email;
  }

  @override
  void didUpdateWidget(covariant MyForm oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initInfo != widget.initInfo) {
      // 同步更新控制器和选中值
      widget.textEditingController.text = widget.initInfo.phone;
      widget.textEditingController1.text = widget.initInfo.email;
      setState(() {
        selected = widget.initInfo.category;
      });
    }
  }

  @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,
          ),
          TextFormField(
            controller: widget.textEditingController1,
            onChanged: widget.onchangeEmail,
          ),
          DropdownButtonFormField2(
            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: selected?.isEmpty == true ? 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 Category.';
              }
              return null;
            },
            onChanged: widget.onchangeCategory,
            onSaved: widget.onchangeCategory,
          )
        ],
      ),
    );
  }
}

class UserInfo {
  final String id;
  String _phone = '';
  String _email = '';
  String _category = '';

  UserInfo() : id = UniqueKey().toString();

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

  void setPhone(String phone) => _phone = phone;
  void setEmail(String email) => _email = email;
  void setCategory(String category) => _category = category;
}
关键改动说明
  • 给UserInfo添加唯一id,避免用索引作为Widget标识,彻底解决Widget复用错误。
  • 重写didUpdateWidget,确保数据变化时子组件的控制器和下拉框状态同步更新。
  • 修正控制器创建逻辑,仅在添加数据项时创建控制器,删除时同步销毁,避免内存泄漏和状态混乱。
  • 移除冗余的listCtrl变量,简化状态管理。

内容的提问来源于stack exchange,提问作者mobix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 03:10:43