Home > database >  AnimatedList call - RangeError (index): Invalid value: Valid value range is empty: 0
AnimatedList call - RangeError (index): Invalid value: Valid value range is empty: 0

Time:07-26

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

  1. Start the app
  2. Hit the " " icon
  3. Type something in and click enter or click the plus icon
  4. click on the todo in the todo-list (to mark it as done)
    -> error is thrown

CodePudding user response:

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

CodePudding user response:

Remove _todos.remove(item); on onTap and it will work.

The listKey.currentState?.removeItem( also removing item with animation, and it misses the item while animating. So we can use animationState to remove item(data)from it.

On your slideIt

Widget slideIt(BuildContext context, int index, Animation<double> animation) {
// ....
 return SlideTransition(
    position: Tween<Offset>(
      begin: const Offset(-1, 0),
      end: const Offset(0, 0),
    ).animate(animation)
      ..addStatusListener((status) {
        if (status == AnimationStatus.dismissed) {
          _todos.remove(item);
        }
      }),
  • Related