GithubHelp home page GithubHelp logo

great_list_view's Introduction

great_list_view

Overview

A Flutter package that includes a powerful, animated and reorderable list view. Just notify the list view of changes in your underlying list and the list view will automatically animate. You can also change the entire list and automatically dispatch the differences detected by the Myers alghoritm. You can also reorder the items of your list, by simply long-tapping the item you want to move or otherwise, for example via a drag handle.

Compared to the AnimatedList, ReorderableListView material widgets or other thid-party libraries, this library offers:

  • it can be both animated and reordered at the same time;
  • it works without necessarily specifying a List object, but simply using index-based builder callbacks;
  • all changes to list view items are gathered and grouped into intervals, so for example you can remove a whole interval of a thousand items with a single remove change without losing in performance;
  • it also works well even with a very long list;
  • the library doesn't use additional widget for items, like Stack, Offstage or Overlay.
  • it is not mandatory to provide a key for each item, because everything works using only indexes;

Example 1

This package also provides a tree adapter to create a tree view without defining a new widget for it, but simply by converting your tree data into a linear list view, animated or not. Your tree data can be any data type, just describe it using a model based on a bunch of callbacks.

Example 2

IMPORTANT!!! This is still an alpha version! This library is constantly evolving and bug fixing, so it may change very often at the moment, sorry.

This library lacks of a bunch of features at the moment:

  • Lack of a feature to create a separated list view (like ListView.separated construtor);
  • No semantics are currently supported;
  • No infinite lists are supported.

I am developing this library in my spare time just for pleasure. Although it may seem on the surface to be an easy library to develop, I assure you that it is instead quite complex. Therefore the time required for development is becoming more and more demanding. Anyone who likes this library can support me by making a donation at will. This will definitely motivate me and push me to bring this library to its completion. I will greatly appreciate your contribution.

Donate

Installing

Add this to your pubspec.yaml file:

dependencies:
  great_list_view: ^0.2.3

and run;

flutter packages get

Automatic Animated List View

The simplest way to create an animated list view that automatically animates to fit the contents of a List is to use the AutomaticAnimatedListView widget. A list of data must be passed to the widget via the list attribute. This widget uses an AnimatedListDiffListDispatcher internally. This class uses the Meyes algorithm that dispatches the differences to the list view after comparing the new list object with the old one. In this regard, it is necessary to pass to the comparator attribute an AnimatedListDiffListBaseComparator object which takes care of comparing an item of the old list with an item of the new list via two methods:

  • sameItem must return true if the two compared items are the same (if the items have their own ID, just check the two IDs are equal);
  • sameContent is called only if sameItem has returned true, and must return true if the item has changed in content (if false is returned, this dispatches a change notification to the list view). You can also use the callback function-based AnimatedListDiffListComparator version, saving you from creating a new derived class.

The list view needs an AnimatedListController object to be controlled. Just instantiate it and pass it to the listController attribute of the AutomaticAnimatedListView widget. The AutomaticAnimatedListView widget uses the controller to notify the changes identified by the Meyes algorithm.

Finally, you have to pass a delegate to the itemBuilder attribute in order to build all item widgets of the list view. The delegate has three parameters:

  • the BuildContext to use to build items;
  • the item of the List, which could be taken from either the old or the new list;
  • an AnimatedWidgetBuilderData object, which provide further interesting information.

The most important attributes of the AnimatedWidgetBuilderData object to consider for sure are:

  • measuring is a flag, and indicates to build an item not in order to be rendered on the screen, but only to measure its extent. This flag must certainly be taken into consideration for performance purposes if the item is a complex widget, as it will certainly be faster to measure an equivalent but simplified widget having the same extent. Furthermore, it is important that the widget does not have animation widgets inside, because the measurement performed must refer to its final state;
  • animation, provides an Animation object to be used to animate the incoming and outcoming effects (which occur when the item is removed or inserted); the value 1 indicates that the item has completely entered the list view, whereas 0 indicates that the item is completely dismissed. Unless you want to customize these animations, you can ignore this attribute.

By default, all animations are automatically wrapped around the item built by the itemBuilder, with the exception of animation which deals with modifying the content of a item, which must be implicit to the widget itself. For example, if the content of the item reflects its size, margins or color, simply wrap the item in an AnimatedContainer: this widget will take care of implicitly animating the item when one of the above attributes changes value.

The itemExtent attribute can be used to set a fixed extent for all items.

If the detectMoves attribute is set to true, the dispatcher will also calculate if there are items that can be moved, rather than removing and inserting them from scratch. I do not recommend enabling this attribute for lists that are too large, as the algorithm used by AnimatedListDiffListDispatcher to determine which items are moved is rather slow.

Example 1 (Automatic Animated List View)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late List<ItemData> currentList;

  @override
  void initState() {
    super.initState();
    currentList = listA;
  }

  void swapList() {
    setState(() {
      if (currentList == listA) {
        currentList = listB;
      } else {
        currentList = listA;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AutomaticAnimatedListView<ItemData>(
        list: currentList,
        comparator: AnimatedListDiffListComparator<ItemData>(
            sameItem: (a, b) => a.id == b.id,
            sameContent: (a, b) =>
                a.color == b.color && a.fixedHeight == b.fixedHeight),
        itemBuilder: (context, item, data) => data.measuring
            ? Container(
                margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60)
            : Item(data: item),
        listController: controller,
        scrollController: scrollController,
        detectMoves: true,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () => gkey.currentState?.swapList(),
        child: AnimatedContainer(
            height: data.fixedHeight ?? 60,
            duration: const Duration(milliseconds: 500),
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              'Item ${data.id}',
              style: TextStyle(fontSize: 16),
            ))));
  }
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData(1, Colors.orange),
  ItemData(2),
  ItemData(3),
  ItemData(4, Colors.cyan),
  ItemData(5),
  ItemData(8, Colors.green)
];
List<ItemData> listB = [
  ItemData(4, Colors.cyan),
  ItemData(2),
  ItemData(6),
  ItemData(5, Colors.pink, 100),
  ItemData(7),
  ItemData(8, Colors.yellowAccent),
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

However, if the changing content cannot be implicitly animated using implicit animations, such as animating a text that is changing, this library also provides the MorphTransition widget, which performs a cross-fade effect between an old widget and the new one. The MorphTransition widget uses a delegate to be passed to the comparator attribute which takes care of comparing the old widget with the new one. This delegate has to return false if the two widgets are different, in order to trigger the cross-fade effect. This comparator needs to be well implemented, because returning false even when not necessary will lead to a drop in performance as this effect would also be applied to two completely identical widgets, thus wasting precious resources to perform an animation that is not actually necessary and that is not even perceptible to the human eye.

More simply, you can pass the delegate directly to the morphComparator attribute of the AutomaticAnimatedListView widget, in this way all items will automatically be wrapped with a MorphTransition widget.

For more features please read the documentation of the AutomaticAnimatedListView class.

Example 2 (Automatic Animated List View with MorphTransition)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late List<ItemData> currentList;

  @override
  void initState() {
    super.initState();
    currentList = listA;
  }

  void swapList() {
    setState(() {
      if (currentList == listA) {
        currentList = listB;
      } else {
        currentList = listA;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AutomaticAnimatedListView<ItemData>(
        list: currentList,
        comparator: AnimatedListDiffListComparator<ItemData>(
            sameItem: (a, b) => a.id == b.id,
            sameContent: (a, b) =>
                a.text == b.text &&
                a.color == b.color &&
                a.fixedHeight == b.fixedHeight),
        itemBuilder: (context, item, data) => data.measuring
            ? Container(
                margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60)
            : Item(data: item),
        listController: controller,
        morphComparator: (a, b) {
          if (a is Item && b is Item) {
            return a.data.text == b.data.text &&
                a.data.color == b.data.color &&
                a.data.fixedHeight == b.data.fixedHeight;
          }
          return false;
        },
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () => gkey.currentState?.swapList(),
        child: Container(
            height: data.fixedHeight ?? 60,
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              data.text,
              style: TextStyle(fontSize: 16),
            ))));
  }
}

class ItemData {
  final int id;
  final String text;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.text, this.id,
      [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData('Text 1', 1, Colors.orange),
  ItemData('Text 2', 2),
  ItemData('Text 3', 3),
  ItemData('Text 4', 4),
  ItemData('Text 5', 5),
  ItemData('Text 8', 8, Colors.green)
];
List<ItemData> listB = [
  ItemData('Text 2', 2),
  ItemData('Text 6', 6),
  ItemData('Other text 5', 5, Colors.pink, 100),
  ItemData('Text 7', 7),
  ItemData('Other text 8', 8, Colors.yellowAccent)
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

Animated List View

If you want to have more control over the list view, or if your data is not just items of a List object, I suggest using the more flexible AnimatedListView widget.

Unlike the AutomaticAnimatedListView widget, the AnimatedListView does not use the Meyes algorithm internally, so all change notifications have to be manually notified to the list view.

As with the AutomaticAnimatedListView widget, you need to pass an AnimatedListController object to the listController attribute.

The widget also needs the initial count of the items, via initialItemCount attribute. This attribute is only used in the very early stage of creating the widget, since the item count will then be automatically derived based on the notifications sent.

The delegate to pass to the itemBuilder attribute has the same purpose as the AutomaticAnimatedListView, however it differs in the second parameter. While the item itself of a List object was passed for the AutomaticAnimatedListView, an index is passed for the AnimatedListView instead. The index of this builder will always refer to the final underlying list, i.e. the list already modified after all the notifications.

Removed or changed items will instead use another builder that will need to be passed to the controller. For example, to notify the list view that the first three items have been removed, you need to call the controller's notifyRemovedRange method with from = 0 and count = 3, and pass it a new builder that only builds the three removed items. The index, which ranges from 0 and 2, will refer in this case only to the three removed items. The other methods notifyChangedRange, notifyInsertedRange, notifyReplacedRange and notifyMovedRange can be used instead to respectfully notify a range of items that have been modified, inserted, replaced or moved.

If you need to send multiple notifications in sequence, it is recommended for performance purposes to invoke the batch method instead, which takes a parameterless delegate as input, and then send all the notifications within it.

Example 3 (Animated List View)

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          floatingActionButton: FloatingActionButton.extended(
              label: Text('Random Change'), onPressed: randomChange),
          body: Body(),
        )));
  }
}

class Body extends StatelessWidget {
  Body({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AnimatedListView(
        initialItemCount: list.length,
        itemBuilder: (context, index, data) => data.measuring
            ? Container(margin: EdgeInsets.all(5), height: 60)
            : Item(data: list[index]),
        listController: controller,
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
        duration: const Duration(milliseconds: 500),
        height: 60,
        margin: EdgeInsets.all(5),
        padding: EdgeInsets.all(15),
        decoration: BoxDecoration(
            color: kColors[data.color],
            border: Border.all(color: Colors.black12, width: 0)),
        child: Center(
            child: Text(
          'Item ${data.id}',
          style: TextStyle(fontSize: 16),
        )));
  }
}

final kColors = const <Color>[
  Colors.teal,
  Colors.lightGreen,
  Colors.redAccent,
  Colors.pink
];

class ItemData {
  final int id;
  final int color;
  const ItemData(this.id, [int color = 0]) : color = color % 4;
}

int id = 0;
List<ItemData> list = [for (var i = 1; i <= 10; i++) ItemData(++id)];

var r = Random();

void randomChange() {
  var activity = list.isEmpty ? 1 : r.nextInt(5);
  switch (activity) {
    case 0: // remove
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final subList = list.sublist(from, to);
      list.removeRange(from, to);
      controller.notifyRemovedRange(from, to - from,
          (context, index, data) => Item(data: subList[index]));
      break;
    case 1: // insert
      final from = r.nextInt(list.length + 1);
      final count = 1 + r.nextInt(5);
      list.insertAll(from, [for (var i = 0; i < count; i++) ItemData(++id)]);
      controller.notifyInsertedRange(from, count);
      break;
    case 2: // replace
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final count = 1 + r.nextInt(5);
      final subList = list.sublist(from, to);
      list.replaceRange(
          from, to, [for (var i = 0; i < count; i++) ItemData(++id)]);
      controller.notifyReplacedRange(from, to - from, count,
          (context, index, data) => Item(data: subList[index]));
      break;
    case 3: // change
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final subList = list.sublist(from, to);
      list.replaceRange(from, to, [
        for (var i = 0; i < to - from; i++)
          ItemData(subList[i].id, subList[i].color + 1)
      ]);
      controller.notifyChangedRange(from, to - from,
          (context, index, data) => Item(data: subList[index]));
      break;
    case 4: // move
      var from = r.nextInt(list.length);
      var count = 1 + r.nextInt(list.length - from);
      var newIndex = r.nextInt(list.length - count + 1);
      var to = from + count;
      final moveList = list.sublist(from, to);
      list.removeRange(from, to);
      list.insertAll(newIndex, moveList);
      controller.notifyMovedRange(from, count, newIndex);
      break;
  }
}

final scrollController = ScrollController();
final controller = AnimatedListController();

It is always possible to manually integrate the Meyes algorithm using an AnimatedListDiffListDispatcher or a more generic AnimatedListDiffDispatcher (if your data are not formed from elements of a List object).

For more features please read the documentation of the AnimatedListView class.

Example 4 (Animated List View with a dispatcher)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late AnimatedListDiffListDispatcher<ItemData> dispatcher;

  @override
  void initState() {
    super.initState();

    dispatcher = AnimatedListDiffListDispatcher<ItemData>(
      controller: controller,
      itemBuilder: itemBuilder,
      currentList: listA,
      comparator: AnimatedListDiffListComparator<ItemData>(
          sameItem: (a, b) => a.id == b.id,
          sameContent: (a, b) =>
              a.color == b.color && a.fixedHeight == b.fixedHeight),
    );
  }

  void swapList() {
    setState(() {
      if (dispatcher.currentList == listA) {
        dispatcher.dispatchNewList(listB, detectMoves: true);
      } else {
        dispatcher.dispatchNewList(listA, detectMoves: true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AnimatedListView(
        initialItemCount: dispatcher.currentList.length,
        itemBuilder: (context, index, data) =>
            itemBuilder(context, dispatcher.currentList[index], data),
        listController: controller,
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () => gkey.currentState?.swapList(),
        child: AnimatedContainer(
            height: data.fixedHeight ?? 60,
            duration: const Duration(milliseconds: 500),
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              'Item ${data.id}',
              style: TextStyle(fontSize: 16),
            ))));
  }
}

Widget itemBuilder(
    BuildContext context, ItemData item, AnimatedWidgetBuilderData data) {
  if (data.measuring) {
    return Container(margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60);
  }
  return Item(data: item);
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData(1, Colors.orange),
  ItemData(2),
  ItemData(3),
  ItemData(4, Colors.cyan),
  ItemData(5),
  ItemData(8, Colors.green)
];
List<ItemData> listB = [
  ItemData(4, Colors.cyan),
  ItemData(2),
  ItemData(6),
  ItemData(5, Colors.pink, 100),
  ItemData(7),
  ItemData(8, Colors.yellowAccent),
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

Animated Sliver List

If the list view consists of multiple slivers, you will need to use the AnimatedSliverList (or AnimatedSliverFixedExtentList if the items all have a fixed extent) class within a CustomScrollView widget.

The AnimatedSliverList only needs two parameters, the usual listController and a delegate.

The AutomaticAnimatedListView and AnimatedListView widgets automatically use these slivers internally with the help of a default delegate implementation offered by the AnimatedSliverChildBuilderDelegate class.

The AnimatedSliverChildBuilderDelegate delegate is more than enough to cover most needs, however, if you need more control, you can always create a new one by extending the AnimatedSliverChildDelegate class. However, I don't recommend extending this class directly unless strictly necessary.

Example 5 (Animated List using slivers)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late AnimatedListDiffListDispatcher<ItemData> dispatcher;

  @override
  void initState() {
    super.initState();

    dispatcher = AnimatedListDiffListDispatcher<ItemData>(
      controller: controller,
      itemBuilder: itemBuilder,
      currentList: listA,
      comparator: AnimatedListDiffListComparator<ItemData>(
          sameItem: (a, b) => a.id == b.id,
          sameContent: (a, b) =>
              a.color == b.color && a.fixedHeight == b.fixedHeight),
    );
  }

  void swapList() {
    setState(() {
      if (dispatcher.currentList == listA) {
        dispatcher.dispatchNewList(listB);
      } else {
        dispatcher.dispatchNewList(listA);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: CustomScrollView(
        controller: scrollController,
        slivers: [
          SliverList(
              delegate: SliverChildBuilderDelegate(
            (BuildContext context, int itemIndex) {
              return Container(
                  alignment: Alignment.center,
                  height: 200,
                  decoration: BoxDecoration(
                      border: Border.all(color: Colors.red, width: 4)),
                  child: ListTile(title: Text('This is another sliver')));
            },
            childCount: 1,
          )),
          AnimatedSliverList(
              controller: controller,
              delegate: AnimatedSliverChildBuilderDelegate(
                  (context, index, data) =>
                      itemBuilder(context, dispatcher.currentList[index], data),
                  dispatcher.currentList.length))
        ],
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () => gkey.currentState?.swapList(),
        child: AnimatedContainer(
            height: data.fixedHeight ?? 60,
            duration: const Duration(milliseconds: 500),
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              'Item ${data.id}',
              style: TextStyle(fontSize: 16),
            ))));
  }
}

Widget itemBuilder(
    BuildContext context, ItemData item, AnimatedWidgetBuilderData data) {
  if (data.measuring) {
    return Container(margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60);
  }
  return Item(data: item);
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData(1, Colors.orange),
  ItemData(2),
  ItemData(3),
  ItemData(4),
  ItemData(5),
  ItemData(8, Colors.green)
];
List<ItemData> listB = [
  ItemData(2),
  ItemData(6),
  ItemData(5, Colors.pink, 100),
  ItemData(7),
  ItemData(8, Colors.yellowAccent)
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

Reordering

The list view can also be reordered on demand, even while it is animating. You can enable the automatic reordering feature, which is activated by long pressing on the item you want to reorder, setting the addLongPressReorderable attribute to true (this attribute can also be found in the AutomaticAnimatedListView, AnimatedListView and AnimatedSliverChildBuilderDelegate classes).

In addition you have to pass a model to the reorderModel attribute by extending the AnimatedListBaseReorderModel class. You can also use the callback function-based AnimatedListReorderModel version, saving you from creating a new derived class.

The fastest way to add support for reordering for all items is to use a AutomaticAnimatedListView widget and pass an instance of the AutomaticAnimatedListReorderModel class (it requires as input the same list you pass to the list view via the list attribute).

Example 6 (Reorderable Automatic Animated List View)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late List<ItemData> currentList;

  @override
  void initState() {
    super.initState();
    currentList = listA;
  }

  void swapList() {
    setState(() {
      if (currentList == listA) {
        currentList = listB;
      } else {
        currentList = listA;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AutomaticAnimatedListView<ItemData>(
        list: currentList,
        comparator: AnimatedListDiffListComparator<ItemData>(
            sameItem: (a, b) => a.id == b.id,
            sameContent: (a, b) =>
                a.color == b.color && a.fixedHeight == b.fixedHeight),
        itemBuilder: (context, item, data) => data.measuring
            ? Container(
                margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60)
            : Item(data: item),
        listController: controller,
        addLongPressReorderable: true,
        reorderModel: AutomaticAnimatedListReorderModel(currentList),
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () => gkey.currentState?.swapList(),
        child: AnimatedContainer(
            height: data.fixedHeight ?? 60,
            duration: const Duration(milliseconds: 500),
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              'Item ${data.id}',
              style: TextStyle(fontSize: 16),
            ))));
  }
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData(1, Colors.orange),
  ItemData(2),
  ItemData(3),
  ItemData(4),
  ItemData(5),
  ItemData(8, Colors.green)
];
List<ItemData> listB = [
  ItemData(2),
  ItemData(6),
  ItemData(5, Colors.pink, 100),
  ItemData(7),
  ItemData(8, Colors.yellowAccent)
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

If you need more control, or if you are not using the AutomaticAnimatedListView widget, you will need to implement the reorder model manually.

The model requires the implementation of four methods.

In order to enable reordering of all items, simply have the onReorderStart callback return true. The function is called with the index of the item to be dragged for reordering, and the coordinates of the exact point touched. The function must return a flag indicating whether the item can be dragged/reordered or not.

To allow the dragged item to be dropped in the new moved position, simply have the onReorderMove callback return true. The function is called with the index of the item being dragged and the index that the item would assume. The function must return a flag indicating whether or not the item can be moved in that new position.

Finally you have to implement the onReorderComplete function to actually move the dragged item. As the onReorderMove method, the function is called by passing it the two indices, and must return true to confirm the swap. If the function returns false, the swap will fail, and the dragged item will return to its original position. The onReorderComplete function is also responsible for actually swapping the two items in the underlying data list when it returns true.

For more details about the model read the documentation of the AnimatedListBaseReorderModel class.

Example 7 (Reorderable Animated List View with custom reorder model)

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          floatingActionButton: FloatingActionButton.extended(
              label: Text('Random Change'), onPressed: randomChange),
          body: Body(),
        )));
  }
}

class Body extends StatelessWidget {
  Body({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AnimatedListView(
        initialItemCount: list.length,
        itemBuilder: (context, index, data) => data.measuring
            ? Container(margin: EdgeInsets.all(5), height: 60)
            : Item(data: list[index]),
        listController: controller,
        addLongPressReorderable: true,
        reorderModel: AnimatedListReorderModel(
          onReorderStart: (index, dx, dy) {
            // only teal-colored items can be reordered
            return list[index].color == 0;
          },
          onReorderMove: (index, dropIndex) {
            // pink-colored items cannot be swapped
            return list[dropIndex].color != 3;
          },
          onReorderComplete: (index, dropIndex, slot) {
            list.insert(dropIndex, list.removeAt(index));
            return true;
          },
        ),
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
        duration: const Duration(milliseconds: 500),
        height: 60,
        margin: EdgeInsets.all(5),
        padding: EdgeInsets.all(15),
        decoration: BoxDecoration(
            color: kColors[data.color],
            border: Border.all(color: Colors.black12, width: 0)),
        child: Center(
            child: Text(
          'Item ${data.id}',
          style: TextStyle(fontSize: 16),
        )));
  }
}

final kColors = const <Color>[
  Colors.teal,
  Colors.lightGreen,
  Colors.redAccent,
  Colors.pink
];

class ItemData {
  final int id;
  final int color;
  const ItemData(this.id, [int color = 0]) : color = color % 4;
}

int id = 0;
List<ItemData> list = [
  for (var i = 1; i <= 10; i++) ItemData(++id, (id - 1) % 4)
];

var r = Random();

void randomChange() {
  var activity = list.isEmpty ? 1 : r.nextInt(4);
  switch (activity) {
    case 0: // remove
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final subList = list.sublist(from, to);
      list.removeRange(from, to);
      controller.notifyRemovedRange(from, to - from,
          (context, index, data) => Item(data: subList[index]));
      break;
    case 1: // insert
      final from = r.nextInt(list.length + 1);
      final count = 1 + r.nextInt(5);
      list.insertAll(from, [for (var i = 0; i < count; i++) ItemData(++id)]);
      controller.notifyInsertedRange(from, count);
      break;
    case 2: // replace
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final count = 1 + r.nextInt(5);
      final subList = list.sublist(from, to);
      list.replaceRange(
          from, to, [for (var i = 0; i < count; i++) ItemData(++id)]);
      controller.notifyReplacedRange(from, to - from, count,
          (context, index, data) => Item(data: subList[index]));
      break;
    case 3: // change
      final from = r.nextInt(list.length);
      final to = from + 1 + r.nextInt(list.length - from);
      final subList = list.sublist(from, to);
      list.replaceRange(from, to, [
        for (var i = 0; i < to - from; i++)
          ItemData(subList[i].id, subList[i].color + 1)
      ]);
      controller.notifyChangedRange(from, to - from,
          (context, index, data) => Item(data: subList[index]));
      break;
  }
}

final scrollController = ScrollController();
final controller = AnimatedListController();

If you want to implement a custom reordering, for example based on dragging an handle instead of long pressing the item, you will have to use the controller again to notify the various steps of the reordering process, calling the notifyStartReorder, notifyUpdateReorder and notifyStopReorder methods.

The notifyStartReorder method must be called first, by passing it as a parameter the build context of the item to be dragged, as well as the coordinates of the point that will be used as the origin point for the translation. In order to translate the item to the new position you have to call the notifyUpdateReorder method, passing it the coordinates of the new point. Finally, to finish the reordering you have to call the notifyStopReorder method.

Example 8 (Reorderable Automatic Animated List View with a handle)

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(key: gkey),
        )));
  }
}

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

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  late List<ItemData> currentList;

  @override
  void initState() {
    super.initState();
    currentList = listA;
  }

  void swapList() {
    setState(() {
      if (currentList == listA) {
        currentList = listB;
      } else {
        currentList = listA;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AutomaticAnimatedListView<ItemData>(
        list: currentList,
        comparator: AnimatedListDiffListComparator<ItemData>(
            sameItem: (a, b) => a.id == b.id,
            sameContent: (a, b) =>
                a.color == b.color && a.fixedHeight == b.fixedHeight),
        itemBuilder: (context, item, data) => data.measuring
            ? Container(
                margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60)
            : Item(data: item),
        listController: controller,
        reorderModel: AutomaticAnimatedListReorderModel(currentList),
        addLongPressReorderable: false,
        scrollController: scrollController,
      ),
    );
  }
}

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      Expanded(
          child: GestureDetector(
              onTap: () => gkey.currentState?.swapList(),
              child: AnimatedContainer(
                  height: data.fixedHeight ?? 60,
                  duration: const Duration(milliseconds: 500),
                  margin: EdgeInsets.all(5),
                  padding: EdgeInsets.all(15),
                  decoration: BoxDecoration(
                      color: data.color,
                      border: Border.all(color: Colors.black12, width: 0)),
                  child: Center(
                      child: Text(
                    'Item ${data.id}',
                    style: TextStyle(fontSize: 16),
                  ))))),
      GestureDetector(
        onVerticalDragStart: (dd) {
          controller.notifyStartReorder(
              context, dd.localPosition.dx, dd.localPosition.dy);
        },
        onVerticalDragUpdate: (dd) {
          controller.notifyUpdateReorder(
              dd.localPosition.dx, dd.localPosition.dy);
        },
        onVerticalDragEnd: (dd) {
          controller.notifyStopReorder(false);
        },
        onVerticalDragCancel: () {
          controller.notifyStopReorder(true);
        },
        child: Icon(Icons.drag_handle),
      )
    ]);
  }
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<ItemData> listA = [
  ItemData(1, Colors.orange),
  ItemData(2),
  ItemData(3),
  ItemData(4),
  ItemData(5),
  ItemData(8, Colors.green)
];
List<ItemData> listB = [
  ItemData(2),
  ItemData(6),
  ItemData(5, Colors.pink, 100),
  ItemData(7),
  ItemData(8, Colors.yellowAccent)
];

final scrollController = ScrollController();
final controller = AnimatedListController();
final gkey = GlobalKey<_BodyState>();

The model also provides the onReorderFeedback method which can be implemented to gain more control over the drag phase. The method is continuously called every time the dragged item is moved not only along the main axis of the list view, but also along the cross axis, passing the delta of the deviation from the origin point. The method have to return an instance of any object (or null) each time. If the object instance is different from the one returned in the previous call, the dragged item will be rebuilded. The last instance returned from the onReorderFeedback method will eventually be passed as the last argument to the onReorderComplete method.

An example of the use of the feedback is shown in the tree list adapter example in order to implement a horizontal drag feature (cross axis), used to change the parent of the node being dragged.

Tree List Adapter

Does you data consist of nodes in a hierarchical tree and you need a tree view to show them? No problem, you can use the TreeistAdapter class to convert the nodes to a linear list. Each node will therefore be an item of a list view corresponding to a specific index. The nodeToIndex and indexToNode methods can be used the former to determine the list index of a particular node and the latter to determine the node corresponding to a given index. The class internally uses a window that shows only a part of the tree properly converted into a linear list. Each time the index of a new node is requested, the window will be moved to contain that node.

In order to perform this conversion, the adapter needs a model that describes the tree. The model is nothing more than a bunch of callback functions that you have to implement. These are:

  • parentOf returns the parent of a node;
  • childrenCount returns the count of the children belonging to a node;
  • childAt returns the child node of a parent node at a specific position;
  • isNodeExpanded returns true if the node is expanded, false if it is collapsed;
  • indexOfChild returns the position of a child node with respect to the parent node;
  • equals returns true if two nodes are equal.

To notify when a particular node is expanded or collapsed, the notifyNodeExpanding and notifyNodeCollapsing methods must be called respectively. It will be necessary to pass the involved node and a callback function that updates the status of the node (expanded / collapsed).

To notify when a particular node is removed from or inserted into the tree, the notifyNodeRemoving and notifyNodeInserting methods must be called respectively. It will be necessary to pass the involved node and a callback function that updates the tree, performing the actual removal or insertion of the node.

Instead, to notify when a node is moved from a certain position to a new one, use the notifyNodeMoving method (see documentation for more details).

This adapter works well also with a normal ListView. However, the adapter also offers the ability to automatically notify changes to an animated list view by simply passing the controller of your list view in the constructor.

Example 9 (Reorderable Tree View)

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';
import 'package:great_list_view/tree_list_adapter.dart';
import 'package:great_list_view/other_widgets.dart';

void main() {
  buildTree(rnd, root, 5, 3);
  Executor().warmUp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(),
        )));
  }
}

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  @override
  Widget build(BuildContext context) {
    return AnimatedListView(
      listController: controller,
      itemBuilder: (context, index, data) =>
          itemBuilder(context, adapter, index, data),
      initialItemCount: adapter.count,
      reorderModel: AnimatedListReorderModel(
        onReorderStart: (index, dx, dy) {
          if (adapter.includeRoot && index == 0) return false;
          var node = adapter.indexToNode(index);
          if (!adapter.isLeaf(node) && adapter.isNodeExpanded(node)) {
            // cannot reorder an open node! the long click must first collapse it
            adapter.notifyNodeCollapsing(node, () {
              collapsedMap.add(node);
            }, index: index, updateNode: true);
            return false;
          }
          return true;
        },
        onReorderFeedback: (fromIndex, toIndex, offset, dx, dy) {
          var level =
              adapter.levelOf(adapter.indexToNode(fromIndex)) + dx ~/ 15.0;
          var levels = adapter.getPossibleLevelsOfMove(fromIndex, toIndex);
          return level.clamp(levels.from, levels.to - 1);
        },
        onReorderMove: (fromIndex, toIndex) {
          return !adapter.includeRoot || toIndex != 0;
        },
        onReorderComplete: (fromIndex, toIndex, slot) {
          var levels = adapter.getPossibleLevelsOfMove(fromIndex, toIndex);
          if (!levels.isIn(slot as int)) return false;
          adapter.notifyNodeMoving(
            fromIndex,
            toIndex,
            slot,
            (pNode, node) => pNode.children.remove(node),
            (pNode, node, pos) => pNode.add(node, pos),
            updateParentNodes: true,
          );
          return true;
        },
      ),
    );
  }
}

class NodeData {
  final String text;
  List<NodeData> children = [];
  NodeData? parent;

  NodeData(this.text);

  void add(NodeData n, [int? index]) {
    (index == null) ? children.add(n) : children.insert(index, n);
    n.parent = this;
  }

  @override
  String toString() => text;
}

class TreeNode extends StatelessWidget {
  final NodeData node;
  final int level;
  final bool? expanded;
  final void Function(bool expanded) onExpandCollapseTap;
  final void Function() onRemoveTap;
  final void Function() onInsertTap;
  TreeNode({
    Key? key,
    required this.node,
    required this.level,
    required this.expanded,
    required this.onExpandCollapseTap,
    required this.onRemoveTap,
    required this.onInsertTap,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      dense: true,
      trailing: _buildExpandCollapseArrowButton(),
      leading: _buildAddRemoveButtons(),
      title: AnimatedContainer(
        padding: EdgeInsets.only(left: level * 15.0),
        duration: const Duration(milliseconds: 250),
        child: Text(node.toString(), style: const TextStyle(fontSize: 14)),
      ),
    );
  }

  Widget _buildAddRemoveButtons() {
    return SizedBox(
        width: 40,
        child: Row(
          children: [
            _buildIconButton(Colors.red, const Icon(Icons.remove), onRemoveTap),
            _buildIconButton(Colors.green, const Icon(Icons.add), onInsertTap),
          ],
        ));
  }

  Widget? _buildExpandCollapseArrowButton() {
    if (expanded == null) return null;
    return ArrowButton(
        expanded: expanded!,
        turns: 0.25,
        icon: const Icon(Icons.keyboard_arrow_right),
        duration: const Duration(milliseconds: 500),
        onTap: onExpandCollapseTap);
  }

  static Widget _buildIconButton(
      Color color, Icon icon, void Function() onPressed) {
    return DecoratedBox(
        decoration: BoxDecoration(border: Border.all(color: color, width: 2)),
        child: SizedBox(
            width: 20,
            height: 25,
            child: IconButton(
                padding: EdgeInsets.all(0.0),
                iconSize: 15,
                icon: icon,
                onPressed: onPressed)));
  }
}

Widget itemBuilder(BuildContext context, TreeListAdapter<NodeData> adapter,
    int index, AnimatedWidgetBuilderData data) {
  final node = adapter.indexToNode(index);
  return TreeNode(
    node: node,
    level: (data.dragging && data.slot != null)
        ? data.slot as int
        : adapter.levelOf(node),
    expanded: adapter.isLeaf(node) ? null : adapter.isNodeExpanded(node),
    onExpandCollapseTap: (expanded) {
      if (expanded) {
        adapter.notifyNodeExpanding(node, () {
          collapsedMap.remove(node);
        }, updateNode: true);
      } else {
        adapter.notifyNodeCollapsing(node, () {
          collapsedMap.add(node);
        }, updateNode: true);
      }
    },
    onRemoveTap: () {
      adapter.notifyNodeRemoving(node, () {
        node.parent!.children.remove(node);
      }, updateParentNode: true);
    },
    onInsertTap: () {
      var newTree = NodeData(kNames[rnd.nextInt(kNames.length)]);
      adapter.notifyNodeInserting(newTree, node, 0, () {
        node.add(newTree, 0);
      }, updateParentNode: true);
    },
  );
}

void buildTree(Random r, NodeData node, int maxChildren, [int? startWith]) {
  var n = startWith ?? (maxChildren > 0 ? r.nextInt(maxChildren) : 0);
  if (n == 0) return;
  for (var i = 0; i < n; i++) {
    var child = NodeData(kNames[r.nextInt(kNames.length)]);
    buildTree(r, child, maxChildren - 1);
    node.add(child);
  }
}

TreeListAdapter<NodeData> adapter = TreeListAdapter<NodeData>(
  childAt: (node, index) => node.children[index],
  childrenCount: (node) => node.children.length,
  parentOf: (node) => node.parent!,
  indexOfChild: (parent, node) => parent.children.indexOf(node),
  isNodeExpanded: (node) => !collapsedMap.contains(node),
  includeRoot: true,
  root: root,
  controller: controller,
  builder: itemBuilder,
);

const List<String> kNames = [
  'Liam',
  'Olivia',
  'Noah',
  'Emma',
  'Oliver',
  'Ava',
  'William',
  'Sophia',
  'Elijah',
  'Isabella',
  'James',
  'Charlotte',
  'Benjamin',
  'Amelia',
  'Lucas',
  'Mia',
  'Mason',
  'Harper',
  'Ethan',
  'Evelyn'
];

final root = NodeData('Me');
final rnd = Random();
final collapsedMap = <NodeData>{};
final controller = AnimatedListController();

Additional useful methods

The AnimatedListController object provides other useful methods for obtaining information about the placement of the items.

Another useful method is computeItemBox which allows you to retrieve the box (position and size) of an item. This method is often used in conjunction with the jumpTo and animateTo methods of a ScrollController to scroll to a certain item.

It is also possible to position at a certain scroll offset when the list view is built for the first time using the initialScrollOffsetCallback attribute of the AnimatedSliverChildDelegate, AnimatedListView and AutomaticAnimatedListView classes; a callback function has to be passed that is invoked at the first layout of the list view, and it has to return the offset to be positioned at the beginning.

Example 10 (Scroll To Index)

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:great_list_view/great_list_view.dart';

void main() {
  Executor().warmUp();
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        home: SafeArea(
            child: Scaffold(
          body: Body(),
        )));
  }
}

class Body extends StatelessWidget {
  Body({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      controller: scrollController,
      child: AutomaticAnimatedListView<ItemData>(
        list: myList,
        listController: controller,
        comparator: AnimatedListDiffListComparator<ItemData>(
            sameItem: (a, b) => a.id == b.id,
            sameContent: (a, b) =>
                a.color == b.color && a.fixedHeight == b.fixedHeight),
        itemBuilder: (context, item, data) => data.measuring
            ? Container(
                margin: EdgeInsets.all(5), height: item.fixedHeight ?? 60)
            : Item(data: item),
        initialScrollOffsetCallback: (c) {
          final i = rnd.nextInt(myList.length);
          final box = controller.computeItemBox(i, true)!;
          print('scrolled to item ${myList[i]}');
          return max(
              0.0, box.top - (c.viewportMainAxisExtent - box.height) / 2.0);
        },
        scrollController: scrollController,
      ),
    );
  }
}

final rnd = Random();

class Item extends StatelessWidget {
  final ItemData data;

  const Item({Key? key, required this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () {
          final listIndex = rnd.nextInt(myList.length);
          final box = controller.computeItemBox(listIndex, true);
          if (box == null) return;
          print('scrolled to item ${myList[listIndex]}');
          final c = context
              .findAncestorRenderObjectOfType<RenderSliver>()!
              .constraints;
          final r = box.top - (c.viewportMainAxisExtent - box.height) / 2.0;
          scrollController.animateTo(r,
              duration: const Duration(milliseconds: 500),
              curve: Curves.easeIn);
        },
        child: AnimatedContainer(
            height: data.fixedHeight ?? 60,
            duration: const Duration(milliseconds: 500),
            margin: EdgeInsets.all(5),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: data.color,
                border: Border.all(color: Colors.black12, width: 0)),
            child: Center(
                child: Text(
              'Item ${data.id}',
              style: TextStyle(fontSize: 16),
            ))));
  }
}

class ItemData {
  final int id;
  final Color color;
  final double? fixedHeight;
  const ItemData(this.id, [this.color = Colors.blue, this.fixedHeight]);
  @override
  String toString() => '$id';
}

int n = 0;

List<ItemData> myList = [
  for (n = 1; n <= 10; n++) ItemData(n, Colors.blue, 60),
  for (; n <= 20; n++) ItemData(n, Colors.orange, 80),
  for (; n <= 30; n++) ItemData(n, Colors.yellow, 50),
  for (; n <= 40; n++) ItemData(n, Colors.red, 120),
];

final scrollController = ScrollController();
final controller = AnimatedListController();
Anyone who likes this library can support me by making a donation at will. This will definitely motivate me and push me to bring this library to its completion. I will greatly appreciate your contribution.

Donate

great_list_view's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

great_list_view's Issues

Add itemExtent

Add itemExtent to AnimatedSliverChildBuilderDelegate and elsewhere, where it's applicable, if there are such places.

Feature request/Doc update request: animationDuration

I was unable to find a way to alter the default animation of item appearance/disappearance, and further "repositioning". The only parameter I found was morphDuration. I'd expect that to influence how long it takes for the same item with different content to make the transition animation.

So if it's currently possible, it would be great to mention it in the documentation. If it's not, I think a lot of lib users would benefit from adding it.

Thanks!

Can this package be used with GridViews?

In my app, I don't need the user to reorder the contents but they are done when some filters are enabled.

Use case:

Grids show cars.
When user presses red color, I want to show only the red colored cars
And I want this filtering to happen in an animated way.
So there might be several cars that are removed from different places of the cars array.
When the filter is removed, all cars should be added in place with ordering animation.

Can I use this package for this need?

Morph Comparitor Parameters are of Type Widget

the type def for morph comparitor is as follows typedef MorphComparator = bool Function(Widget a, Widget b);
this leave me unable to compare if the data the widget represet has chnaged.

would changing it to typedef MorphComparator = bool Function(dynamic a, dynamic b); work? that way i can type cast it to anything i need.

Console throw exception when call `controller.dispatchNewList(...)` to update an item in list

Hi,

Like the title, when I call controller.dispatchNewList(...) to update one item in the current list view, the below exception was thrown on the console, the app still working just like normal. So I don't know is this important or not.

══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during paint():
'package:flutter/src/rendering/layer.dart': Failed assertion: line 270 pos 12: '!_debugDisposed': is
not true.

Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.md

The relevant error-causing widget was:
  MorphTransition
  MorphTransition:file:///Users/macbookair/Workspace/Personal/todolist/lib/utils/task_animated_list.dart:63:12

When the exception was thrown, this was the stack:
#2      Layer.markNeedsAddToScene (package:flutter/src/rendering/layer.dart:270:12)
#3      OpacityLayer.alpha= (package:flutter/src/rendering/layer.dart:1882:7)
#4      PaintingContext.pushOpacity (package:flutter/src/rendering/object.dart:617:9)

Thanks a lot 🍻

Issue: setState() or markNeedsBuild() called when widget tree was locked.

If an item in AutomaticAnimatedListView has an SVG image rendered by the flutter_svg library, I'm getting exceptions.

After a bit of googling it seems to be a more common issue than just with the SVG library. E.g. https://stackoverflow.com/questions/60852896/widget-cannot-be-marked-as-needing-to-build-because-the-framework-is-already-in

The library I'm using is flutter_svg: ^2.0.1:
https://pub.dev/packages/flutter_svg

Stack trace:

======== Exception caught by SVG ===================================================================
The following assertion was thrown by a synchronously-called image listener:
setState() or markNeedsBuild() called when widget tree was locked.

This SvgPicture widget cannot be marked as needing to build because the framework is locked.
The widget on which setState() or markNeedsBuild() was called was: SvgPicture
  dirty
  dependencies: [DefaultTextStyle, Directionality, _LocalizationsScope-[GlobalKey#1c2fb]]
  state: _SvgPictureState#b3489(lifecycle state: initialized, stream: PictureStream#f79df(OneFramePictureStreamCompleter#c6c6f, Instance of 'PictureInfo', 7 listeners, cached))
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4636:9)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4646:6)
#2      State.setState (package:flutter/src/widgets/framework.dart:1153:15)
#3      _SvgPictureState._handleImageChanged (package:flutter_svg/svg.dart:814:5)
#4      PictureStreamCompleter.addListener (package:flutter_svg/src/picture_stream.dart:297:17)
#5      PictureStream.addListener (package:flutter_svg/src/picture_stream.dart:204:26)
#6      _SvgPictureState._listenToStream (package:flutter_svg/svg.dart:843:21)
#7      _SvgPictureState.didChangeDependencies (package:flutter_svg/svg.dart:752:5)
#8      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5119:11)
#9      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4944:5)
...     Normal element mounting (7 frames)
#16     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#17     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6512:36)
#18     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6524:32)
#19     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#20     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6512:36)
#21     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6524:32)
#22     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#23     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6512:36)
#24     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6524:32)
#25     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#26     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6512:36)
#27     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6524:32)
...     Normal element mounting (7 frames)
#34     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#35     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6512:36)
#36     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6524:32)
...     Normal element mounting (254 frames)
#290    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3953:16)
#291    Element.updateChild (package:flutter/src/widgets/framework.dart:3682:18)
#292    AnimatedSliverMultiBoxAdaptorElement.updateChild (package:great_list_view/src/child_manager.dart:552:28)
#293    AnimatedSliverMultiBoxAdaptorElement.disposableElement.<anonymous closure> (package:great_list_view/src/child_manager.dart:651:36)
#294    BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2601:15)
#295    AnimatedSliverMultiBoxAdaptorElement.disposableElement (package:great_list_view/src/child_manager.dart:647:12)
#296    AnimatedRenderSliverList.measureItem (package:great_list_view/src/sliver_list.dart:1011:18)
#297    AnimatedRenderSliverList.measureItems.<anonymous closure> (package:great_list_view/src/sliver_list.dart:998:17)
#302    _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:26)
(elided 4 frames from class _Timer, dart:async, and dart:async-patch)
====================================================================================================

PS: Thanks for the library, might become an awesome alternative to Android's ListView with auto diff animations :)

Qn: pass data as props

Thank you for your work on this project.I have slightly different need, where the data is passed as props. Please see the example. Everything works fine, except there is no animation. Any feedback is greatly appreciated.
Thanks again.
bsr.

Eidt: there is a refresh button on top right (it's blue, so sometimes hard to see :-))which toggles the lists, and diff is notified of change in didUpdateWidget by the parent.

import 'package:flutter/material.dart';

import 'package:great_list_view/great_list_view.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:worker_manager/worker_manager.dart' show Executor;

void main() async {
  await Executor().warmUp();
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late List<MyItem> items;

  @override
  void initState() {
    super.initState();

    items = listA;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test App',
        theme: ThemeData(
          primarySwatch: Colors.yellow,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: SafeArea(
            child: Scaffold(
          body: Stack(
            children: <Widget>[
              MyListView(items),
              Align(
                alignment: Alignment.topRight,
                child: GestureDetector(
                  onTap: click,
                  child: Icon(
                    Icons.refresh,
                    color: Colors.blue,
                    size: 36.0,
                  ),
                ),
              ),
            ],
          ),
        )));
  }

  void click() {
    setState(() {
      items = swapList ? listB : listA;
    });
    swapList = !swapList;
  }
}

class MyItem {
  final int id;
  final Color color;
  final double? fixedHeight;
  const MyItem(this.id, [this.color = Colors.blue, this.fixedHeight]);
}

List<MyItem> listA = [
  MyItem(1, Colors.orange),
  MyItem(2),
  MyItem(3),
  MyItem(4),
  MyItem(5),
  MyItem(8, Colors.green)
];
List<MyItem> listB = [
  MyItem(2),
  MyItem(6),
  MyItem(5, Colors.pink, 100),
  MyItem(7),
  MyItem(8, Colors.yellowAccent)
];

AnimatedListController controller = AnimatedListController();

class MyComparator extends ListAnimatedListDiffComparator<MyItem> {
  MyComparator._();

  static MyComparator instance = MyComparator._();

  @override
  bool sameItem(MyItem a, MyItem b) => a.id == b.id;

  @override
  bool sameContent(MyItem a, MyItem b) =>
      a.color == b.color && a.fixedHeight == b.fixedHeight;
}

bool swapList = true;

class MyListView extends StatefulWidget {
  final List<MyItem> items;
  MyListView(this.items);

  @override
  _MyListViewState createState() => _MyListViewState();
}

class _MyListViewState extends State<MyListView> {
  late ListAnimatedListDiffDispatcher<MyItem> diff;

  @override
  void initState() {
    super.initState();
    diff = ListAnimatedListDiffDispatcher<MyItem>(
      animatedListController: controller,
      currentList: widget.items,
      itemBuilder: buildItem,
      comparator: MyComparator.instance,
    );
  }

  @override
  void didUpdateWidget(covariant MyListView oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.items != oldWidget.items) {
      diff.dispatchNewList(widget.items);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
        child: CustomScrollView(
      slivers: <Widget>[
        AnimatedSliverList(
          delegate: AnimatedSliverChildBuilderDelegate(
            (BuildContext context, int index, AnimatedListBuildType buildType,
                [dynamic slot]) {
              return buildItem(
                  context, diff.currentList[index], index, buildType);
            },
            childCount: () => diff.currentList.length,
            onReorderStart: (i, dx, dy) => true,
            onReorderMove: (i, j) => true,
            onReorderComplete: (i, j, slot) {
              var list = diff.currentList;
              var el = list.removeAt(i);
              list.insert(j, el);
              return true;
            },
          ),
          controller: controller,
          reorderable: true,
        )
      ],
    ));
  }

  Widget buildItem(BuildContext context, MyItem item, int index,
      AnimatedListBuildType buildType) {
    return SizedBox(
        height: item.fixedHeight,
        child: DecoratedBox(
            key: buildType == AnimatedListBuildType.NORMAL
                ? ValueKey(item)
                : null,
            decoration: BoxDecoration(
                border: Border.all(color: Colors.black12, width: 0)),
            child: Container(
                color: item.color,
                margin: EdgeInsets.all(5),
                padding: EdgeInsets.all(15),
                child: Center(
                    child: Text(
                  'Item ${item.id}',
                  style: TextStyle(fontSize: 16),
                )))));
  }
}

Adding Dividers / Separators

Not an issue but a feature request: is it possible to be able to add dividers, as in ListView.separated?
I can't find any way or even workaround to achieve that.

Doing that by hand just by adding, let's say, a Divider on top (or bottom) of each item except the first one (or last one) leads to bugs when deleting the first (or last) item.

This is something really missing, in my opinion.

Thanks.

_currentList will not updated when the controller is not attached to the view

Problem: When working with lazy loadings widgets and an new state of the list has already triggerd via dispatchNewList, it will still show the old state of the list.
Example: Working with the BloC Pattern and a TabBarView. Usually you get the new data of the list over a listener and fire dispatchNewList. But the works only for the current visible tab view. when switching to an other tab, you will still the the old state of the list.

Solution:
Update the _currentList even when the controller is not attached to an view, but do not perform any animation.

Version: 0.0.10
File: animaeted_list_dispatcher.dart
Method: dispatchNewList LOC: 60

original code:
if (!animatedListController.isAttached) return;

bug fix:
if(!animatedListController.isAttached){ _oldList = currentList; _currentList = newList; return; }

If you want, I can create also a PR, but didn't found any hints how you wanna work with pr :D
Actually I just made the fix locale and it works now nice.

BTW: Thx, for the package. Before I had my own solution with Animation and Reorder but yeah it was not quite so good like yours. Had Problems with smooth scrolling during the drag&drop.

`dispatchNewList` the whole list data passed as props or getting from API

Thanks for your awesome work!

The plugin worked well and really easy to use.
But the first time I was stuck when I get the new list data from API and then passed the whole list to dispatchNewList, in this case there is no animation occurred.
The workaround is manually comparing the old list and the new list to find the diff, and then push/remove the different items to the old list.

Not working:

var newList = await tasksDao.fetchTasks();

taskController.dispatchNewList(newList);

Working:

var removedTask = ... // Finding the diff
var newList = taskController.currentList.where((element) => element.id != removedTask.id).toList();

taskController.dispatchNewList(newList);

I think this issue will be a big improvement when it has resolved because this is frequently behaviour in my opinion.

Thanks a lot 🍻

AutomaticAnimatedListView grey tint when reorderModel specified

I am using the AutomaticAnimatedListView, and when the reorderModel is set to something other than null, I get this behaviour where the list items have a greyish background. Why is this?

class BrowseListView extends StatelessWidget {
  final BrowseMode mode;

  const BrowseListView({Key? key, required this.mode}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final list = mode.decksToList.toList();
    return Scrollbar(
      child: AutomaticAnimatedListView(
        list: list,
        listController: context.read<MainViewModel>().listController,
        itemBuilder: (context, item, data) =>
            data.measuring ? nothing : BrowseItemWidget(item, list),
        comparator: AnimatedListDiffListComparator<DeckListItem>(
          sameItem: (a, b) => a.runtimeType == b.runtimeType && a.id == b.id,
          sameContent: (a, b) => false,
        ),
        addLongPressReorderable: true,
        reorderModel: AutomaticAnimatedListReorderModel(list),
      ),
    );
  }
}

class BrowseItemWidget extends StatelessWidget {
  final DeckListItem item;
  final List<DeckListItem> items;

  const BrowseItemWidget(this.item, this.items, {super.key});

  @override
  Widget build(BuildContext context) {
    final model = context.read<MainViewModel>();
    return Column(
      children: [
        ListTile(
          //leading: leading(context, model),
          title: title(context),
          subtitle: subtitle(context),
          trailing: checkBox(context, model),
          onTap: () => model.openItem(item),
          minLeadingWidth: 0,
        ),
        const Divider(height: 1)
      ],
    );
  }

  Widget leading(BuildContext context, MainViewModel model) {
    return Container(
      height: double.infinity,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          RoundCheckBox(
            size: 20,
            isChecked: item.selected,
            uncheckedColor: Colors.transparent,
            checkedColor: context.accentColorFaded,
            checkedWidget: const Padding(
              padding: EdgeInsets.all(4),
              child: Icon(Icons.check, size: 10),
            ),
            onTap: (_) => model.toggleSelection(item),
          ),
        ],
      ),
    );
  }

  Widget checkBox(BuildContext context, MainViewModel model) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 50,
          height: 20,
          alignment: Alignment.center,
          decoration: const BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(10)),
              color: Colors.red),
          child: AutoSizeText(
            '${item.dueCount}',
            minFontSize: 5,
            style: const TextStyle(color: Colors.white),
          ),
        ),
        const SizedBox(width: 15),
        PopupMenuButton(
          position: PopupMenuPosition.under,
          constraints: const BoxConstraints(),
          padding: EdgeInsets.zero,
          iconSize: 20,
          itemBuilder: (_) {
            return [
              PopupMenuItem(
                onTap: model.deselectAll,
                child: Text(context.str.deselectAll),
              )
            ];
          },
          child: Icon(Icons.more_vert_outlined, color: context.accentColor),
        ),
      ],
    );
  }

  Widget title(BuildContext context) => Text(
        item.name,
        style: Theme.of(context).textTheme.titleMedium!.copyWith(
            fontWeight: item is Folder ? FontWeight.bold : FontWeight.normal),
      );

  Widget? subtitle(BuildContext context) => item.description.isEmpty
      ? null
      : Text(
          item.description,
          style: Theme.of(context)
              .textTheme
              .titleSmall!
              .copyWith(fontWeight: FontWeight.w300),
        );
}

class ListReorder extends AutomaticAnimatedListReorderModel<DeckListItem> {
  final MainViewModel model;

  ListReorder(this.model, super.list);

  @override
  bool onReorderComplete(int index, int dropIndex, Object? slot) {
    list.insert(dropIndex, list.removeAt(index));
    final newList = list.indexed
        .map((pair) => pair.$2.cloneWithListOrder(pair.$1))
        .toList();
    model.saveList(newList);
    return true;
  }
}

image_2023-06-25_040702987

white background when addLongPressReorderable: true

if List items have no background or have transparent backgrounds, adding the addLongPressReorderable option will introduce a white background for the list items.

example using
class Item extends StatelessWidget {
@OverRide
Widget build(BuildContext context) {
return AnimatedContainer(
height: height,
duration: const Duration(milliseconds: 500),
decoration: const BoxDecoration(
color: Colors.transparent,
),
child: Text("hello"),
);
}

Have I missed some option to style the reorder item widget?

in addition: When adding a reorderModel the addLongPressReorderable flag is supurflous: the longpress functionaliy is there anyway.

Error on compile

I am getting error on compile app with great_list_view.

[+8433 ms] ../../.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/dispatcher.dart:160:32: Error: Expected 6 type arguments.
[        ]       _cancelable = Executor().execute<T, T, AnimatedListDiffBaseComparator<T>,
[        ]                                ^
[+2765 ms] ../../.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/dispatcher.dart:160:32: Error: Expected 6 type arguments.
[        ]       _cancelable = Executor().execute<T, T, AnimatedListDiffBaseComparator<T>,
[        ]                                ^

flutter doctor:

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.3.3, on Ubuntu 22.04.1 LTS 5.15.0-48-generic, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Chrome - develop for the web
[✓] Linux toolchain - develop for Linux desktop
[✓] Android Studio (version 2021.3)
[✓] VS Code
[✓] Connected device (3 available)
[✓] HTTP Host Availability

• No issues found!

Seems related to new work_manager package.

[Feature request] Support `move` item in `AnimatedSliverList`

Hi,

I think this is a known issue because I found this line in your code:

move: (from, to) {
throw 'operation noy supported yet!';
},

Currently, when I programmatically change the position of an item to a new position, two animations occurred:

  • One for remove that item from the old position
  • One for insert that item into the new position

I think it will be awesome and more convenient if the item is moving from the old position to the new position just like the reorderable drag behavior.

If you don't have time to implement this feature, I can help with a PR but please give me some advice and direction on this feature.

Thanks 🍻

DragListener Widget

is there any widget wrapper that acts as a listener for drags? just like flutter's ReorderableDragStartListener

A way to make scrolling/non-scrolling behavior resulting from adding a new item a bit more predictable

When you insert an item just a small bit above the the items that're currently visible, sometimes everything visible will be displaced downwards by it. This seems like a mistake to me. If I'm not scrolled in the position where I would see the new item come in, I don't want the appearance of the new item to move the things I'm looking at around.

If great_list_view had been used to make a chat window, for instance, if the user is scrolled up by any amount at all, they have communicated to the app that they're reading chat backlog and that they don't want new messages to disrupt their reading by making things move. That applies to every use case I can think of for inserting items out of view. Currently, great_list_view would do the wrong thing there a lot of time.

Crash when calling setState(() => dispatcher.dispatchNewList(...))

In my Flutter project, I use the great_list_view library, following the Example 5 in the ReadMe.

Sometimes, I need to update items, so I call:

setState(() => dispatcher.dispatchNewList(myNewList));

But for some reason, I get the following error:

setState() callback argument returned a Future.
The setState() method on ListeTitresCleState#30d55 was called with a closure or method that returned a Future. Maybe it is marked as "async".
Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().

What is really surprising is that I only get that error when I run my app in Debug Mode (not in Profile / Release Mode)!

Can I get some help on that?

Thanks.

Infinite Scrolling Pagination

Thanks for the awesome package. Makes animating and sorting list so easy.
Will infinite scrolling pagination be part of the lib? Or do we have to handle it ourselves?
Hopefully the package being called what it is, could handle it internally :)

Custom Animations such as Resizing

Hello,

I'm not exactly sure if this is a feature yet or not, but I wanted to implement a different animation for the list, such as when an item is being moved, for example move from index 5 to 0,

  • index 5 item resizes to 0 width and 0 anchored at the center
  • previous items move forward to fill space
  • index 5 resizes up to regular width/height at index 0

Is this possible at all with this library? If not any ideas on how to implement it? I'd be willing to submit a PR if there are pointers on how to go about it

Exception on dispose after animation using Flutter 3.7.0

Hi, i keep getting various exceptions when modifying the list.
The following exception throws on dispose, after adding an item to the list, after the animation is complete.
Update: The issue seems to be related to Flutter 3.7.0

_AssertionError ('package:flutter/src/foundation/change_notifier.dart': Failed assertion: line 325 pos 7: '_notificationCallStackDepth == 0': The "dispose()" method on attachedTo: {}, time: 1.0, value: 1.0 DISPOSED was called during the call to "notifyListeners()". This is likely to cause errors since it modifies the list of listeners while the list is being used.)

Code snippet:

final animatedListControllerProvider =
    Provider.autoDispose<AnimatedListController>(
        (ref) => AnimatedListController());

class GreatListViewContent extends ConsumerWidget {
  const GreatListViewContent({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(listNotifierProvider);
    final listController = ref.watch(animatedListControllerProvider);

    return AutomaticAnimatedListView<int>(
      physics: const NeverScrollableScrollPhysics(),
      list: state.list,
      comparator: AnimatedListDiffListComparator<int>(
          sameItem: (a, b) => a == b, sameContent: (a, b) => a == b),
      itemBuilder: (context, item, data) =>
          Card(child: ListTile(title: Text(item.toString()))),
      listController: listController,
      detectMoves: true,
      shrinkWrap: true,
    );
  }
}

Stack:

_AssertionError._throwNew (dart:core-patch/errors_patch.dart:40)
ChangeNotifier.dispose (c:\Users\Joi\fvm\versions\3.7.0\packages\flutter\lib\src\foundation\change_notifier.dart:325)
_ControlledAnimation.dispose (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\animation.dart:112)
_ControlledAnimation.detachFrom (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\animation.dart:102)
_AnimatedSpaceInterval.dispose (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\intervals.dart:539)
_IntervalList.replace.<anonymous closure> (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\interval_list.dart:143)
Iterable.forEach (dart:core/iterable.dart:325)
_IntervalList.replace (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\interval_list.dart:141)
_IntervalManager.onIntervalCompleted (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\interval_manager.dart:852)
_IntervalManager._createAnimation.<anonymous closure>.<anonymous closure> (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\interval_manager.dart:926)
List.forEach (dart:core-patch/growable_array.dart:416)
_IntervalManager._createAnimation.<anonymous closure> (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\interval_manager.dart:924)
ChangeNotifier.notifyListeners (c:\Users\Joi\fvm\versions\3.7.0\packages\flutter\lib\src\foundation\change_notifier.dart:381)
_ControlledAnimation._onCompleted (c:\Users\Joi\AppData\Local\Pub\Cache\hosted\pub.dev\great_list_view-0.2.0+2\lib\src\animation.dart:139)
_RootZone.run (dart:async/zone.dart:1654)
_FutureListener.handleWhenComplete (dart:async/future_impl.dart:190)
Future._propagateToListeners.handleWhenCompleteCallback (dart:async/future_impl.dart:737)
Future._propagateToListeners (dart:async/future_impl.dart:793)
Future._completeWithValue (dart:async/future_impl.dart:567)```

Issue: List item freezes when reordering an item to last position in Reorderable Tree View (Example 9)

In the Reorderable Tree View (using Example 9 provided in the readme), when taking an item and moving it to the last slot of the parent item, the item freezes and an Exception is thrown:

======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
'package:great_list_view/tree_list_adapter.dart': Failed assertion: line 318 pos 12: 'position >= 0 && position <= childrenCount(parentNode)': is not true.

When the exception was thrown, this was the stack: 
#2      TreeListAdapter.notifyNodeInserting (package:great_list_view/tree_list_adapter.dart:318:12)
#3      TreeListAdapter.notifyNodeMoving (package:great_list_view/tree_list_adapter.dart:473:5)
#4      Body.build.<anonymous closure> (package:poc_list/main.dart:67:19)
#5      AnimatedListReorderModel.onReorderComplete (package:great_list_view/src/widgets.dart:548:27)
#6      AnimatedRenderSliverMultiBoxAdaptor.reorderStop (package:great_list_view/src/sliver_list.dart:346:52)
#7      AnimatedSliverMultiBoxAdaptorElement.notifyStopReorder (package:great_list_view/src/child_manager.dart:787:18)
#8      AnimatedListController.notifyStopReorder (package:great_list_view/src/child_manager.dart:1045:17)
#9      LongPressReorderable._onLongPressEnd (package:great_list_view/src/widgets.dart:470:17)
#10     LongPressReorderable.build.<anonymous closure> (package:great_list_view/src/widgets.dart:477:32)
#11     LongPressGestureRecognizer._checkLongPressEnd.<anonymous closure> (package:flutter/src/gestures/long_press.dart:798:71)
#12     GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:253:24)
#13     LongPressGestureRecognizer._checkLongPressEnd (package:flutter/src/gestures/long_press.dart:798:11)
#14     LongPressGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/long_press.dart:636:9)
#15     PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:615:9)
#16     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
#17     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9)
#18     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:625:13)
#19     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
#20     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
#21     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:460:19)
#22     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:440:22)
#23     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:336:11)
#24     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:395:7)
#25     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:357:5)
#26     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:314:7)
#27     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:295:7)
#28     _invoke1 (dart:ui/hooks.dart:164:13)
#29     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:361:7)
#30     _dispatchPointerDataPacket (dart:ui/hooks.dart:91:31)
(elided 2 frames from class _AssertionError)
Handler: "onLongPressEnd"
Recognizer: LongPressGestureRecognizer#1cefa
  debugOwner: GestureDetector
  state: possible
====================================================================================================

Screenshot_1692872520

It is still possible to add and remove items from the list by using the "+" and "-" buttons except for the item that is frozen.

Screenshot_1692872528

Changing an attribute of an item not working

After reading your ReadMe, I tried the Example 5.
In that example, there is the following, which works fine:

void swapList() {
    setState(() {
      if (dispatcher.currentList == listA) {
        dispatcher.dispatchNewList(listB);
      } else {
        dispatcher.dispatchNewList(listA);
      }
    });
  }

OK, now let's say that I want to just change the value of the color of one item in the list, so here is what I tried:

void swapList() {
    setState(() {
      dispatcher.currentList[0] = ItemData(1, Colors.black);
      dispatcher.dispatchNewList(dispatcher.currentList);
    });
  }

But for some reason, nothing happens: the first item of the list isn't refreshed.

After some trials and errors, I found out that I have to duplicate the list and create a new instance for the first item, like the following, to make it work:

void swapList() {
    setState(() {
      List<ItemData> listAbis = [...dispatcher.currentList];
      listAbis[0] = ItemData(1, Colors.black);
      dispatcher.dispatchNewList(listAbis);
    });
  }

But why? Is this the expected behavior?

Bad dependency worker_manager: ">=4.0.2"

Error: Expected 6 type arguments.
../…/src/dispatcher.dart:160
          _cancelable = Executor().execute<T, T, AnimatedListDiffBaseComparator<T>,

You must limit the max version of dependencies. The new version of the worker_manager has different interface of the Executor than you're expecting at the moment.

Is this package still being maintained?

Flutter 3: "Warning: Operand of null-aware operation '!' has type 'WidgetsBinding' which excludes null."

In my Flutter project, I just upgraded to Flutter 3, and now, when I compile my app, I get the following warnings:

../../../FlutterSDK/flutter/.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/core/sliver_list.dart:29:24: Warning: Operand of null-aware operation '?.' has type 'WidgetsBinding' which excludes null.
 - 'WidgetsBinding' is from 'package:flutter/src/widgets/binding.dart' ('../../../FlutterSDK/flutter/packages/flutter/lib/src/widgets/binding.dart').
    if (WidgetsBinding.instance?.schedulerPhase ==
                       ^
../../../FlutterSDK/flutter/.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/core/sliver_list.dart:31:22: Warning: Operand of null-aware operation '?.' has type 'WidgetsBinding' which excludes null.
 - 'WidgetsBinding' is from 'package:flutter/src/widgets/binding.dart' ('../../../FlutterSDK/flutter/packages/flutter/lib/src/widgets/binding.dart').
      WidgetsBinding.instance?.addPostFrameCallback((_) => markNeedsLayout());
                     ^
../../../FlutterSDK/flutter/.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/core/sliver_list.dart:171:22: Warning: Operand of null-aware operation '?.' has type 'WidgetsBinding' which excludes null.
 - 'WidgetsBinding' is from 'package:flutter/src/widgets/binding.dart' ('../../../FlutterSDK/flutter/packages/flutter/lib/src/widgets/binding.dart').
      WidgetsBinding.instance?.addPostFrameCallback((_) {
                     ^
../../../FlutterSDK/flutter/.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/core/sliver_list.dart:491:20: Warning: Operand of null-aware operation '!' has type 'WidgetsBinding' which excludes null.
 - 'WidgetsBinding' is from 'package:flutter/src/widgets/binding.dart' ('../../../FlutterSDK/flutter/packages/flutter/lib/src/widgets/binding.dart').
    WidgetsBinding.instance!.addPostFrameCallback((_) {
                   ^
../../../FlutterSDK/flutter/.pub-cache/hosted/pub.dartlang.org/great_list_view-0.1.4/lib/src/core/sliver_list.dart:510:24: Warning: Operand of null-aware operation '!' has type 'WidgetsBinding' which excludes null.
 - 'WidgetsBinding' is from 'package:flutter/src/widgets/binding.dart' ('../../../FlutterSDK/flutter/packages/flutter/lib/src/widgets/binding.dart').
        WidgetsBinding.instance!.addPostFrameCallback((_) {
                       ^

And here is my flutter doctor -v output:

[✓] Flutter (Channel stable, 3.0.2, on macOS 11.3.1 20E241 darwin-arm, locale fr-FR)
    • Flutter version 3.0.2 at /Users/mregnauld/FlutterSDK/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision cd41fdd495 (4 days ago), 2022-06-08 09:52:13 -0700
    • Engine revision f15f824b57
    • Dart version 2.17.3
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc1)
    • Android SDK at /Users/mregnauld/AndroidSDK
    • Platform android-32, build-tools 33.0.0-rc1
    • ANDROID_HOME = /Users/mregnauld/AndroidSDK
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.2

[✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.57.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.24.0

[✓] Connected device (2 available)
    • SM M515F (mobile) • RF8NC0PSMMV • android-arm64 • Android 12 (API 31)
    • macOS (desktop)   • macos       • darwin-arm64  • macOS 11.3.1 20E241 darwin-arm
    ! Error: Failed to prepare device for development. Please unlock and reconnect the device. (code 806)

[✓] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 1 category.

Did I miss something, or could it be fixed soon?

Thanks.

Reodrder on click

Is it possible to enable reordering after clicking and holding for a Duration we passed? I need a function for pinning the items, when I click and secure order to the beginning of the list, my items have the necessary structure, but I still don't understand how I can do that, if that's possible with this list.

Feature request: headers

Hi,
it would be cool to have possibility of adding header to the list which wouldn't be taken into consideration while list is dispatching its animations (it would keep its position on the top no matter what is happenning with the rest of the data)

feature request: scroll to index

Hello,

do you think it would be possible to implement a scrollToIndex or initialIndex feature where I could programmatically scroll to specific index? I have a list of widgets ordered by date and I need to initially show today's widgets. I couldn't figure it out. Widgets don't have fixed size.

There are libraries capable of scrolling to index, but the itemBuilder doesn't expose the index afaik.

An error: "You are trying to bind this controller to multiple animated list views"

I found out that the problem with FutureBuilder.

When I use it i get an error: "You are trying to bind this controller to multiple animated list views.
A AnimatedListController can only be binded to one list view at a time".

Could you help me?

BlocBuilder<CategoriesPageBloc, CategoriesPageState>(
          bloc: _categoriesPageBloc,
          builder: (context, state) {
            return FutureBuilder<List<Category>>(
                  future: _futureCategories,
                  builder: (context, snapshot) {
                    return AutomaticAnimatedListView<Category>(
                        //  ...
                    );
                  },
                );
          })

Breaks LayoutBuilder

Using a LayoutBuilder in AnimatedListView's itemBuilder results in an exception during layout:

======== Exception caught by rendering library =====================================================
The following assertion was thrown during performLayout():
'package:flutter/src/rendering/object.dart': Failed assertion: line 1027 pos 12: '_debugDoingLayout': is not true.


Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.md

The relevant error-causing widget was: 
  LayoutBuilder LayoutBuilder:file:///home/ping/IdeaProjects/ftest_bean/lib/main_paint_layout_error2.dart:50:20
When the exception was thrown, this was the stack: 
#2      PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:1027:12)
#3      RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:2246:14)
#4      RenderConstrainedLayoutBuilder.rebuildIfNecessary (package:flutter/src/widgets/layout_builder.dart:228:7)
#5      _RenderLayoutBuilder.performLayout (package:flutter/src/widgets/layout_builder.dart:316:5)
#6      RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#7      RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#8      RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14)
#9      RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#10     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#11     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14)
#12     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#13     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#14     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14)
#15     RenderCustomPaint.performLayout (package:flutter/src/rendering/custom_paint.dart:552:11)
#16     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#17     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#18     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14)
#19     _RenderCustomClip.performLayout (package:flutter/src/rendering/proxy_box.dart:1467:11)
#20     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#21     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#22     RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:249:12)
#23     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#24     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#25     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14)
#26     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#27     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#28     AnimatedRenderSliverList.measureItem.<anonymous closure> (package:great_list_view/src/sliver_list.dart:1013:17)
#29     AnimatedSliverMultiBoxAdaptorElement.disposableElement.<anonymous closure> (package:great_list_view/src/child_manager.dart:653:16)
#30     BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2523:15)
#31     AnimatedSliverMultiBoxAdaptorElement.disposableElement (package:great_list_view/src/child_manager.dart:647:12)
#32     AnimatedRenderSliverList.measureItem (package:great_list_view/src/sliver_list.dart:1012:18)
#33     AnimatedRenderSliverList.measureItems.<anonymous closure> (package:great_list_view/src/sliver_list.dart:999:17)
(elided 7 frames from class _AssertionError, class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch)
The following RenderObject was being processed when the exception was fired: _RenderLayoutBuilder#7498c relayoutBoundary=up9 NEEDS-LAYOUT NEEDS-PAINT
...  parentData: <none> (can use size)
...  constraints: BoxConstraints(w=1264.0, 0.0<=h<=Infinity)
...  size: MISSING
RenderObject: _RenderLayoutBuilder#7498c relayoutBoundary=up9 NEEDS-LAYOUT NEEDS-PAINT
  parentData: <none> (can use size)
  constraints: BoxConstraints(w=1264.0, 0.0<=h<=Infinity)
  size: MISSING
====================================================================================================
import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final listController = AnimatedListController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('LayoutBuilder crash'),
        actions: [
          IconButton(
            onPressed: () {
              listController.notifyInsertedRange(0, 1);
            },
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: AnimatedListView(
        listController: listController,
        itemBuilder: (context, index, data) {
          return Card(
            margin: const EdgeInsets.all(8),
            child: LayoutBuilder(builder: (context, constraints) {
              return Row(
                children: [
                  const Icon(Icons.note),
                  Center(child: Text("index $index")),
                ],
              );
            }),
          );
        },
        initialItemCount: 0,
      ),
    );
  }
}

Related: pingbird/boxy#20

How to use proxyDecorator like in flutter's ReorderableListView

Flutter's ReorderableListView allows us to specify the proxyDecorator property, which allows us to style the item when it is being reordered. We can specify things like elevation, borderRadius, shadowColor etc. My list item component has rounded border and I want to the shadow to have rounded borders as well when we start dragging the item. Does this package provide any such option?

Issue during reorder: Concurrent modification during iteration: Instance(length:0) of '_GrowableList'.

Hi, i have tried to run example and i am getting error on lattest verios 0.1.3:

======== Exception caught by widgets library =======================================================
The following ConcurrentModificationError was thrown while rebuilding dirty elements:
Concurrent modification during iteration: Instance(length:0) of '_GrowableList'.

The relevant error-causing widget was: 
  AutomaticAnimatedListView<ItemData> AutomaticAnimatedListView:file:///home/dimka/ttt/great_list_view/example/lib/main.dart:56:14
When the exception was thrown, this was the stack: 
#0      ListIterator.moveNext (dart:_internal/iterable.dart:336:7)
#1      AnimatedSliverMultiBoxAdaptorElement.performRebuild (package:great_list_view/src/core/child_manager.dart:145:45)
#2      Element.rebuild (package:flutter/src/widgets/framework.dart:4311:5)
#3      BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2578:33)
#4      WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
#5      RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#6      SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1145:15)
#7      SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1082:9)
#8      SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:996:5)
#12     _invoke (dart:ui/hooks.dart:150:10)
#13     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:270:5)
#14     _drawFrame (dart:ui/hooks.dart:114:31)
(elided 3 frames from dart:async)
The element being rebuilt at the time was index 0 of 1: AnimatedSliverList
  dependencies: [_ScrollableScope, _EffectiveTickerMode]
  renderObject: AnimatedRenderSliverList#c1b64 relayoutBoundary=up2 NEEDS-LAYOUT
====================================================================================================

Unable to run example

Nice list view, it seems pretty great!

I was unable to run the example, got a lot of errors from worker_manager dependency

Logs
flutter run -d chrome --web-renderer html --debug --no-sound-null-safety
Launching lib/main.dart on Chrome in debug mode...
../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manager-4
.0.1/lib/src/task.dart:12:27: Error: The parameter 'runnable' can't have a value
of 'null' because of its type 'Runnable<A, B, C, D, O>', but the implicit
default value is 'null'.
 - 'Runnable' is from 'package:worker_manager/src/runnable.dart'        
 ('../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manage
 r-4.0.1/lib/src/runnable.dart').
Try adding either an explicit non-'null' default value or the 'required'
modifier.
  Task(this.number, {this.runnable, this.workPriority});                
                          ^^^^^^^^                                      
../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manager-4
.0.1/lib/src/task.dart:12:42: Error: The parameter 'workPriority' can't have a
value of 'null' because of its type 'WorkPriority', but the implicit default
value is 'null'.
 - 'WorkPriority' is from 'package:worker_manager/src/work_priority.dart'
 ('../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manage
 r-4.0.1/lib/src/work_priority.dart').
Try adding either an explicit non-'null' default value or the 'required'
modifier.
  Task(this.number, {this.runnable, this.workPriority});                
                                         ^^^^^^^^^^^^                   
../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manager-4
.0.1/lib/src/executor.dart:81:23: Error: The argument type 'num' can't be
assigned to the parameter type 'int'.
    final task = Task(_taskNumber,                                      
                      ^                                                 
../../../development/flutter/.pub-cache/hosted/pub.dartlang.org/worker_manager-4
.0.1/lib/src/executor.dart:156:7: Error: The argument type 'num' can't be
assigned to the parameter type 'int'.
      _taskNumber,                                                      
      ^                                                                 
Waiting for connection from debug service on Chrome...             18.2s
Failed to compile application.

How can I limit drag&drop to a specific position range?

In my project, I have a list of 100 items, all reorderable, except the first 2 items.
I need 2 things:

  • the first 2 items should not be reorderable (if I do a long press, nothing should happen)
  • the other items shoud be moveable, but not to the position 0 or 1 (they should always stay after the first 2 items)

How can I achieve that?

Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.