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

Flutter表单列表移除指定索引项异常问题求助

问题:Flutter ListView.separated删除指定项后UI错误删除最后一项

使用ListView.separated展示报告观测项列表,添加功能正常,但删除指定索引的观测项时,打印数据显示选中项已移除,UI却总是删除最后一项。

原始代码

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

  static const routeName = '/add-report';

  @override
  State<AddReportScreen> createState() => _AddReportScreenState();
}

class _AddReportScreenState extends State<AddReportScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  Map _report = {
    'observations': <Observation>[
      const Observation(observation: ''),
    ]
  };


// Add a new observation to the list
  void _addNewObservation() {
    setState(() {
      _report['observations'].add(Observation(observation: ''));
    });
  }


// REset the form
  void _resetForm() {
    setState(() {
      _formKey.currentState!.reset();
      _report = {
        'observations': <Observation>[
          const Observation(observation: ''),
        ]
      };
    });
  }


// Remove observation function
  void _removeObservation(int index) {
    setState(() {
      final obs = _report['observations'];
      obs.removeAt(index);
      _report['observations'] = obs;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add Report'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // Submit the form
            print('Valid');
            print([
              for (var observation in _report['observations'])
                '${observation.observation}'
            ]);
            _resetForm();
          }
        },
        child: const Icon(Icons.add),
      ),
      body: Form(
        key: _formKey,
        child: Column(
          children: [
            Flexible(
              child: ListView.separated(
                shrinkWrap: true,
                itemBuilder: (BuildContext context, index) {
                  return Row(
                    key: Key(index.toString()),
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Expanded(
                        flex: 4,
                        child: TextFormField(
                          onChanged: (value) {
                            _report['observations'][index] = 
                                Observation(observation: value);
                          },
                          decoration: InputDecoration(
                            hintText: "Observation ${index + 1}",
                          ),
                          validator: (String? value) {
                            if (value == null || value.isEmpty) {
                              return 'Required field.';
                            }
                            return null;
                          },
                        ),
                      ),
                      if (_report['observations'].length > 1)
                        Expanded(
                            child: ElevatedButton(
                          onPressed: () {
                            // Remove the observation
                            _removeObservation(index);
                          },
                          style: ButtonStyle(
                            backgroundColor: 
                                MaterialStateProperty.all<Color>(Colors.red),
                            shape: MaterialStateProperty.all<OutlinedBorder>(
                                const CircleBorder()),
                          ),
                          child: const Icon(Icons.remove),
                        ))
                    ],
                  );
                },
                itemCount: _report['observations'].length,
                separatorBuilder: (BuildContext context, index) => 
                    const Divider(),
              ),
            ),
            const SizedBox(
              height: 10.0,
            ),
            ElevatedButton(
              onPressed: () {
                _addNewObservation();
              },
              style: ButtonStyle(
                minimumSize: MaterialStateProperty.all<Size>(
                  const Size(
                    double.infinity,
                    50.0,
                  ),
                ),
              ),
              child: const Text('Add Observation'),
            ),
            const SizedBox(
              height: 10.0,
            ),
            const ElevatedButton(onPressed: null, child: Text("Add Images")),
          ],
        ),
      ),
    );
  }
}

问题原因

核心问题在于列表项的key使用了索引值:Key(index.toString())。Flutter通过key识别列表中每个组件的唯一性,当删除中间项后,后续项的索引会自动前移,Flutter会认为是最后一个组件被移除(因为索引对应的组件发生了变化),从而错误地更新UI。

解决方案

1. 给Observation类添加唯一标识

修改Observation类,增加一个唯一id字段,确保每个观测项有独一无二的标识:

class Observation {
  final String id;
  final String observation;

  const Observation({
    required this.id,
    required this.observation,
  });

  // 可选:添加copyWith方法方便更新数据
  Observation copyWith({String? observation}) {
    return Observation(
      id: id,
      observation: observation ?? this.observation,
    );
  }
}

2. 初始化及添加项时生成唯一id

更新初始化和添加方法,为每个新Observation生成唯一id(可使用时间戳或第三方uuid库):

// 初始化
Map _report = {
  'observations': <Observation>[
    Observation(
      id: DateTime.now().microsecondsSinceEpoch.toString(),
      observation: ''
    ),
  ]
};

// 添加新观测项
void _addNewObservation() {
  setState(() {
    _report['observations'].add(
      Observation(
        id: DateTime.now().microsecondsSinceEpoch.toString(),
        observation: '',
      ),
    );
  });
}

3. 修改列表项的key为基于唯一id

将Row的key从索引改为Observation的id:

return Row(
  key: Key(_report['observations'][index].id), // 使用唯一id作为key
  crossAxisAlignment: CrossAxisAlignment.end,
  // ... 其他代码保持不变
);

4. 优化TextFormField的onChanged逻辑(可选)

使用copyWith方法更新观测项,保持不可变操作:

onChanged: (value) {
  setState(() {
    _report['observations'][index] = _report['observations'][index].copyWith(observation: value);
  });
},

验证修改

修改后,删除指定索引项时,Flutter会通过唯一id正确识别每个组件,UI会准确移除选中的项,不再出现删除最后一项的错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 05:57:30