ListView中Dismissible用index设置Key时抛出异常求助
key: Key(people[index].id.toString())时触发列表为空,抛出“Invalid value: Valid value range is empty: 0”异常 我被这个问题困扰多日,请问为何设置key: Key(people[index].id.toString())时会触发列表为空的情况,进而抛出异常“Invalid value: Valid value range is empty: 0”?相关代码如下:
Person.dart
import 'package:flutter/material.dart'; class Person { int id = UniqueKey().hashCode; late String personName; late double personWeight; late double personHeight; late double personBMI; bool personHasGoodBMI = false; String get name => personName; set name(String name) => personName = name; double get weight => personWeight; set weight(double weight) => personWeight = weight; double get height => personHeight; set height(double height) => personHeight = height; double get bmi => personBMI; set bmi(double bmi) => personBMI = bmi; bool get isValidPerson => personName.isNotEmpty && personWeight > 0 && personHeight > 0; set isValidPerson(bool isValidPerson) => isValidPerson = isValidPerson; bool get hasGoodBMI => personHasGoodBMI; set hasGoodBMI(bool hasGoodBMI) => personHasGoodBMI = hasGoodBMI; Person({this.personName = '', this.personWeight = 0, this.personHeight = 0, this.personBMI = 0}); }
person_CRUD.dart
import 'package:d1_calculadoraimc/models/person.dart'; class PersonCRUD { final List<Person> _persons = []; Future<void> addPerson(Person person) async { await Future.delayed(const Duration(microseconds: 200)); _persons.add(person); } Future<void> removePerson(Person person) async { await Future.delayed(const Duration(microseconds: 200)); _persons.remove(person); } Future<void> updatePerson(String id, bool isGood) async { await Future.delayed(const Duration(microseconds: 200)); final person = _persons.firstWhere((element) => element.id == id); person.hasGoodBMI = isGood; } Future<List<Person>> listPersons() async { await Future.delayed(const Duration(microseconds: 200)); return _persons; } }
calculate_IMC.dart
class IMCCalculator { Person person = Person(); static double calculate(Person person) { double heightInMeters = person.height / 100.0; return person.weight / (heightInMeters * heightInMeters); } Person returnIMCPerson(double height, double weight) { person.height = height; person.weight = weight; person.bmi = calculate(person); person.bmi = double.parse(person.bmi.toStringAsFixed(2)); return person; } }
主文件
import 'package:d1_calculadoraimc/person_CRUD.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'calculate_IMC.dart'; void main() { runApp(MaterialApp( title: 'Calculadora IMC', theme: ThemeData( primarySwatch: Colors.blue, textTheme: const TextTheme( titleLarge: TextStyle( fontSize: 24.0, fontWeight: FontWeight.bold, ), ), ), home: const _CalculateIMC(), )); } class _CalculateIMC extends StatefulWidget { const _CalculateIMC({Key? key}) : super(key: key); @override _CalculateIMCState createState() => _CalculateIMCState(); } class _CalculateIMCState extends State<_CalculateIMC> { final _formKey = GlobalKey<FormState>(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _heightController = TextEditingController(); final TextEditingController _weightController = TextEditingController(); var crud = PersonCRUD(); var people = <Person>[]; IMCCalculator imcCalculator = IMCCalculator(); Person person = Person(); void getPeople() async { people = await crud.listPersons(); setState(() {}); } @override void initState() { getPeople(); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Calculadora IMC'), ), floatingActionButton: FloatingActionButton( onPressed: () { person.name = ""; person.height = 0.0; person.weight = 0.0; showDialog( context: context, builder: (BuildContext bc) => AlertDialog( title: const Text("Enviar informações"), content: Form( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( key: _formKey, controller: _nameController, decoration: const InputDecoration( labelText: 'Nome', ), validator: (value) { if (value == null || value.isEmpty) { return 'Por favor, insira um nome'; } return null; }, ), TextFormField( key: _formKey, controller: _heightController, decoration: const InputDecoration( labelText: 'Altura (cm)', ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], validator: (value) { if (value == null || value.isEmpty) { return 'Por favor, insira uma altura'; } return null; }, ), TextFormField( key: _formKey, controller: _weightController, decoration: const InputDecoration( labelText: 'Peso (kg)', ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], validator: (value) { if (value == null || value.isEmpty) { return 'Por favor, insira um peso'; } return null; }, ), ], ), ), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Icon(Icons.cancel_rounded), ), ElevatedButton( onPressed: () async { if (person.isValidPerson) { await crud.addPerson(Person( personName: _nameController.text, personHeight: double.parse(_heightController.text), personWeight: double.parse(_weightController.text), personBMI: imcCalculator .returnIMCPerson( double.parse(_heightController.text), double.parse(_weightController.text)) .bmi .toDouble())); setState(() { getPeople(); }); Navigator.of(context).pop(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Preencha todos os campos"), ), ); } setState(() { getPeople(); }); }, child: const Icon(Icons.send_rounded), ), ], ), ); }, child: const Icon(Icons.add_rounded), ), body: Container( margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Column(children: [ Expanded(child: ListView.builder(itemBuilder: (context, index) { return Dismissible( key: Key(people[index].id.toString()), onDismissed: (DismissDirection direction) async { await crud.removePerson(people[index]); getPeople(); }, child: ListTile( title: Text( people[index].name, style: const TextStyle(fontSize: 24), ), subtitle: Text( "IMC: ${people[index].bmi}", style: const TextStyle(fontSize: 16), ), trailing: Icon( people[index].hasGoodBMI ? Icons.sentiment_satisfied_alt_rounded : Icons.sentiment_dissatisfied_rounded, color: people[index].hasGoodBMI ? Colors.green : Colors.red, ), ), ); })) ]))); } }
核心原因与问题梳理
ListView.builder缺少itemCount参数
这是触发报错的直接原因:ListView.builder如果不指定itemCount,会默认尝试构建无限多的列表项,当people列表为空时,index=0就会超出列表的有效范围(空列表的有效索引范围是空的),直接抛出越界异常。和你设置的Key无关,只是刚好在访问people[index]时触发了这个问题。表单验证逻辑完全错误
你在添加按钮的判断条件里用了person.isValidPerson,但这个person是State类里的成员实例,你每次打开弹窗都会把它重置为空值,导致验证永远不通过,新的Person根本没被添加到列表里,people一直是空的,这就加重了前面的越界问题。异步更新的冗余调用
在添加Person后,你连续调用了两次setState(() { getPeople(); }),而且getPeople内部已经有setState,这会导致不必要的重绘,也可能引发状态不一致。其他潜在问题
- PersonCRUD的
updatePerson方法参数是String id,但Person类的id是int类型,这里类型不匹配,永远找不到对应的Person; - IMCCalculator的
returnIMCPerson方法复用了类成员的Person实例,多次调用会污染之前的数据。
- PersonCRUD的
解决方案
1. 给ListView.builder添加itemCount
修改ListView.builder部分:
Expanded(child: ListView.builder( itemCount: people.length, // 必须添加这一行 itemBuilder: (context, index) { return Dismissible( key: Key(people[index].id.toString()), onDismissed: (DismissDirection direction) async { await crud.removePerson(people[index]); await getPeople(); // 等待数据更新后再刷新 }, child: ListTile( title: Text( people[index].name, style: const TextStyle(fontSize: 24), ), subtitle: Text( "IMC: ${people[index].bmi}", style: const TextStyle(fontSize: 16), ), trailing: Icon( people[index].hasGoodBMI ? Icons.sentiment_satisfied_alt_rounded : Icons.sentiment_dissatisfied_rounded, color: people[index].hasGoodBMI ? Colors.green : Colors.red, ), ), ); } ))
2. 修复表单验证逻辑
修改添加按钮的onPressed方法:
ElevatedButton( onPressed: () async { // 根据表单输入创建临时Person实例,用于验证和添加 final tempPerson = Person( personName: _nameController.text, personHeight: double.tryParse(_heightController.text) ?? 0, personWeight: double.tryParse(_weightController.text) ?? 0, ); if (tempPerson.isValidPerson) { // 计算IMC final imc = IMCCalculator.calculate(tempPerson); tempPerson.bmi = double.parse(imc.toStringAsFixed(2)); await crud.addPerson(tempPerson); await getPeople(); // 等待数据更新后刷新 Navigator.of(context).pop(); // 清空输入框 _nameController.clear(); _heightController.clear(); _weightController.clear(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Preencha todos os campos corretamente")), ); } }, child: const Icon(Icons.send_rounded), ),
3. 修正PersonCRUD的updatePerson方法
Future<void> updatePerson(int id, bool isGood) async { // 参数改为int id await Future.delayed(const Duration(microseconds: 200)); final person = _persons.firstWhere((element) => element.id == id); person.hasGoodBMI = isGood; }
4. 重构IMCCalculator避免状态污染
class IMCCalculator { static double calculate(Person person) { double heightInMeters = person.height / 100.0; return person.weight / (heightInMeters * heightInMeters); } // 不需要类成员变量,直接返回新的Person实例 static Person returnIMCPerson(double height, double weight) { final person = Person(); person.height = height; person.weight = weight; person.bmi = calculate(person); person.bmi = double.parse(person.bmi.toStringAsFixed(2)); return person; } }
5. 优化getPeople方法
确保异步更新后正确刷新状态:
Future<void> getPeople() async { // 改为Future<void>,方便await people = await crud.listPersons(); setState(() {}); }
总结
最直接解决当前报错的是给ListView添加itemCount: people.length,这样当列表为空时,ListView不会尝试构建任何项,也就不会触发越界异常。同时修复表单验证逻辑,确保新的Person能被正确添加到列表中,其他的优化则是解决潜在的bug,避免后续出现其他问题。
内容的提问来源于stack exchange,提问作者Caio

