简体   繁体   中英

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

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

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM