Flutter AnimatedList标记最后一个待办完成时触发RangeError求助
问题描述
标记最后一个待办事项为已完成时触发RangeError,错误信息:
The following RangeError was thrown building:
RangeError (index): Invalid value: Valid value range is empty: 0When 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); }, ), ); } }
关键修改点
- 修改
slideIt方法参数,直接传入item而非index,避免依赖_todos数组的索引访问 - 在
onTap中先保存待删除的deletedItem,确保removeItem回调能正确获取该item,不受后续数组修改影响 - 所有修改
_todos或_doneTodos的操作包裹在setState中,确保UI正确更新
内容的提问来源于stack exchange,提问作者Hkrie

