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

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,
                    ),
                  ),
                );
              }))
            ])));
  }
}

核心原因与问题梳理

  1. ListView.builder缺少itemCount参数
    这是触发报错的直接原因:ListView.builder如果不指定itemCount,会默认尝试构建无限多的列表项,当people列表为空时,index=0就会超出列表的有效范围(空列表的有效索引范围是空的),直接抛出越界异常。和你设置的Key无关,只是刚好在访问people[index]时触发了这个问题。

  2. 表单验证逻辑完全错误
    你在添加按钮的判断条件里用了person.isValidPerson,但这个person是State类里的成员实例,你每次打开弹窗都会把它重置为空值,导致验证永远不通过,新的Person根本没被添加到列表里,people一直是空的,这就加重了前面的越界问题。

  3. 异步更新的冗余调用
    在添加Person后,你连续调用了两次setState(() { getPeople(); }),而且getPeople内部已经有setState,这会导致不必要的重绘,也可能引发状态不一致。

  4. 其他潜在问题

    • PersonCRUD的updatePerson方法参数是String id,但Person类的id是int类型,这里类型不匹配,永远找不到对应的Person;
    • IMCCalculator的returnIMCPerson方法复用了类成员的Person实例,多次调用会污染之前的数据。

解决方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 12:05:54