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

Flutter AnimatedList标记最后一个待办完成时触发RangeError求助

Flutter待办列表RangeError问题修复

问题描述

标记最后一个待办事项为已完成时触发RangeError,错误信息:

The following RangeError was thrown building:
RangeError (index): Invalid value: Valid value range is empty: 0

When the exception was thrown, this was the stack:
#0 List.[] (dart:core-patch/growable_array.dart:264:36)
#1 _TodoListState.slideIt (package:todo_list/main.dart:38:25)
#2 _TodoListState.slideIt.. (package:todo_list/main.dart:59:59)

错误复现步骤:

  • 启动应用
  • 点击"+"图标
  • 输入内容后按回车或点击加号添加待办
  • 点击该待办标记为已完成,触发错误

问题原因

删除最后一个待办时,removeItem的回调仍调用slideIt(context, index, animation),但此时_todos数组已被清空,slideIt中尝试访问_todos[index]触发数组越界。因为removeItem的回调在动画执行过程中调用,而你在调用removeItem后立刻修改了_todos数组,导致回调执行时数组已为空。

修复方案

在removeItem的回调中直接使用当前要删除的item,而非从_todos数组中获取。修改onTap方法时先保存当前item,再执行删除操作,回调里用保存的item构建动画组件,避免依赖已修改的_todos数组。

修复后的完整代码

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Todo List',
        theme: ThemeData(
          appBarTheme: const AppBarTheme(
            backgroundColor: Colors.white,
            foregroundColor: Colors.blue,
          ),
        ),
        home: const TodoList());
  }
}

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

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
  final _todos = <String>[];
  final _doneTodos = <String>[];
  final _biggerFont = const TextStyle(fontSize: 18);

  Widget slideIt(BuildContext context, String item, Animation<double> animation) {
    final alreadyDone = _doneTodos.contains(item);
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(-1, 0),
        end: const Offset(0, 0),
      ).animate(animation),
      child: SizedBox(
          child: ListTile(
            title: Text(
              item,
              style: _biggerFont,
            ),
            trailing: Icon(
              alreadyDone ? Icons.check_box : Icons.check_box_outline_blank,
              color: alreadyDone ? Colors.blue : null,
              semanticLabel: alreadyDone ? "Remove from done" : "Finish",
            ),
            onTap: () {
              _doneTodos.add(item);
              final currentIndex = _todos.indexOf(item);
              // 保存待删除项,避免数组修改后无法获取
              final deletedItem = item;
              listKey.currentState?.removeItem(
                currentIndex,
                (_, animation) => slideIt(context, deletedItem, animation),
                duration: const Duration(milliseconds: 500),
              );
              setState(() {
                _todos.remove(item);
              });
            },
          )),
    );
  }

  void _addTodo() {
    Navigator.of(context).push(MaterialPageRoute<void>(builder: (context) {
      final myController = TextEditingController();

      return Scaffold(
          appBar: AppBar(
            title: const Text('Add Todo'),
          ),
          body: Container(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                TextField(
                  onSubmitted: (textValue) {
                    setState(() {
                      listKey.currentState!.insertItem(_todos.length,
                          duration: const Duration(milliseconds: 500));
                      _todos.add(textValue);
                    });
                    Navigator.of(context).pop();
                  },
                  autofocus: true,
                  decoration: const InputDecoration(
                    border: UnderlineInputBorder(),
                    labelText: 'Enter a new Thing to do',
                  ),
                  controller: myController,
                ),
                const SizedBox(height: 16),
                IconButton(
                    color: Colors.blue,
                    onPressed: () {
                      setState(() {
                        listKey.currentState!.insertItem(0,
                            duration: const Duration(milliseconds: 500));
                        _todos.insert(0, myController.text);
                      });
                      Navigator.of(context).pop();
                    },
                    icon: const Icon(Icons.add))
              ],
            ),
          ));
    }));
  }

  void _showDoneTodo() {
    Navigator.of(context).push(MaterialPageRoute<void>(builder: (context) {
      final tiles = _doneTodos.map((string) {
        return ListTile(
          trailing: const Icon(
            Icons.check_box,
            semanticLabel: "Remove from done",
          ),
          onTap: () {
            setState(() {
              listKey.currentState!.insertItem(_todos.length,
                  duration: const Duration(milliseconds: 500));
              _todos.add(string);
              _doneTodos.remove(string);
            });
            Navigator.of(context).pop();
          },
          title: Text(
            string,
            style: _biggerFont,
          ),
        );
      });
      final divided = tiles.isNotEmpty
          ? ListTile.divideTiles(context: context, tiles: tiles).toList()
          : <Widget>[];
      return Scaffold(
        appBar: AppBar(
          title: const Text('Done Todos'),
        ),
        body: ListView(children: divided),
      );
    }));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
        actions: [
          IconButton(
            onPressed: _addTodo,
            icon: const Icon(Icons.add),
            tooltip: 'Add Todo',
          ),
          IconButton(
            onPressed: _showDoneTodo,
            icon: const Icon(Icons.check_box),
            tooltip: 'Show Done Todos',
          )
        ],
      ),
      body: AnimatedList(
        key: listKey,
        initialItemCount: _todos.length,
        itemBuilder: (context, index, animation) {
          return slideIt(context, _todos[index], animation);
        },
      ),
    );
  }
}

关键修改点

  1. 修改slideIt方法参数,直接传入item而非index,避免依赖_todos数组的索引访问
  2. 在onTap中先保存待删除的deletedItem,确保removeItem回调能正确获取该item,不受后续数组修改影响
  3. 所有修改_todos或_doneTodos的操作包裹在setState中,确保UI正确更新

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:39:18