I'm currently trying to familiarize myself with flutter. Therefore I am trying to create a todo list. Everything is working fine except that I get an range Error when I mark the last todo as done:
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.<anonymous closure>.<anonymous closure> (package:todo_list/main.dart:59:59)
It seems like flutter is trying to build the slide It widget, even though the list is empty.
Sourcecode (i uploaded it to github as well https://github.com/Hkrie/todo-app ):
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, int index, animation) {
String item = _todos[index];
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);
listKey.currentState?.removeItem(
_todos.indexOf(item), (_, animation) => slideIt(context, index, animation),
duration: const Duration(milliseconds: 500));
_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) {
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: () {
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: () {
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, index, animation);
},
),
);
}
}
The project ist based on the default flutter project, as well as the AnimatedList descriped by Pinkesh Darji here:
https://medium.com/flutter-community/how-to-animate-items-in-list-using-animatedlist-in-flutter-9b1a64e9aa16
How to get the error message :
0. Pull the repo from github
Remove _todos.remove(item);
on onTap
and it will work.
The listKey.currentState?.removeItem(
also removing item with animation, and it misses the item I think. Use like
onTap: () {
_doneTodos.add(item);
listKey.currentState?.removeItem(
index,
(_, animation) => slideIt(context, index, animation),
duration: const Duration(milliseconds: 500),
);
},
The answer is a combination of the answer provided by Yeasin Sheikh and some of my experimenting and searching (especially this post was quite helpfull: Flutter: animate item removal in ListView )
The onTap function has to be changed like so:
onTap: () {
_doneTodos.add(item);
int removeIndex = _todos.indexOf(item);
String removedItem = _todos.removeAt(removeIndex);
listKey.currentState?.removeItem(
removeIndex, (_, animation) => slideIt(context,removedItem, index, animation),
duration: const Duration(milliseconds: 500));
},
Furthermore the rest of the code has to be updated accordingly.
Complete code:
import 'package:flutter/material.dart';
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? removedItem, int index, animation) {
String item = removedItem ?? _todos[index];
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 + _todos.length.toString(),
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);
int removeIndex = _todos.indexOf(item);
String removedItem = _todos.removeAt(removeIndex);
listKey.currentState?.removeItem(
removeIndex, (_, animation) => slideIt(context,removedItem, index, animation),
duration: const Duration(milliseconds: 500));
},
)),
);
}
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) {
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: () {
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: () {
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, null, index, animation);
},
),
);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.