expandable_reorderable_list 1.2.0 copy "expandable_reorderable_list: ^1.2.0" to clipboard
expandable_reorderable_list: ^1.2.0 copied to clipboard

A wrapper around ReorderableListView that allows you to expand and collapse items.

example/lib/main.dart

import 'package:example/lead_or_tail.dart';
import 'package:example/nested_item.dart';
import 'package:expandable_reorderable_list/expandable_reorderable_list.dart';
import 'package:flutter/material.dart';

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

/// A app widget that shows how to use [ExpandableReorderableList].
class App extends StatelessWidget {
  /// A app widget that shows how to use [ExpandableReorderableList].
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Expandable reorderable list'),
        ),
        body: Builder(builder: (context) {
          return Theme(
            data: Theme.of(context).copyWith(
              canvasColor: Colors.transparent,
              shadowColor: Colors.transparent,
            ),
            child: const Home(),
          );
        }),
      ),
    );
  }
}

/// A sample widget that shows how to use [ExpandableReorderableList].
class Home extends StatefulWidget {
  /// A sample widget that shows how to use [ExpandableReorderableList].
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  /// The nested item that are displayed.
  final nestedItems = NestedItem.generate(depth: 3, width: 4);

  /// All the items.
  final nestedItemsMap = <String, NestedItem>{};

  final modelsController =
      ExpandableReorderableListItemModelController<ValueKey<String>>();

  @override
  void initState() {
    super.initState();
    nestedItemsMap.addAll(nestedItems.toMap());
  }

  /// It is called when an item is dragged and dropped.
  ///
  /// The `from` and `to` are the indexes of the items are available with
  /// [onReorderParam.oldIndex] and [onReorderParam.newIndex].
  void onReorder(OnReorderParam<ValueKey<String>> onReorderParam) {
    final item = onReorderParam.item;
    final nestedItem = nestedItemsMap[item.key.value]!;
    if (onReorderParam.oldIndex == 0) {
      // The first item is moved.
      // It is only a valid drag if:
      // - The item is collapsed
      // - The next item level is the same as the item level.
      if (!(modelsController.items[item.key]!.isCollapsed ||
          onReorderParam.newNextItem?.level == item.level)) {
        return;
      }
    }
    if (onReorderParam.newPreviousItem == null) {
      // The dragged item is moved at the first position.
      if (item.level != 0) {
        // The dragged item is not a top level item. This is not a valid drop.
        // If dropped at the first position, the item won't have a parent
        // anymore.
        return;
      }
      try {
        nestedItem.remove(modelsController);
        nestedItems.insert(0, nestedItem);
        setState(() {});
      } on StateError {
        // Invalid drag and drop operation.
      }
    } else {
      // The new previous item is not null.
      final newPreviousItem = onReorderParam.newPreviousItem!;
      final newPreviousNestedItem = nestedItemsMap[newPreviousItem.key.value]!;
      if (item.level == newPreviousItem.level) {
        // The item is moved after an item of the same level. It needs to be
        // inserted in the previous item's children just after the previous
        // item.
        try {
          nestedItem.remove(modelsController);
          newPreviousNestedItem.parent!.insert(
            newPreviousNestedItem.indexInParent + 1,
            nestedItem,
          );
          if (!modelsController.items[newPreviousItem.key]!.isCollapsed &&
              !modelsController.items[item.key]!.isCollapsed) {
            // The new previous item and the item are expanded. We need to pass
            // the new previous item's children to the moved item.
            nestedItem.children.addAll(newPreviousNestedItem.children);
            newPreviousNestedItem.children.clear();
          }
          setState(() {});
        } on StateError {
          // Invalid drag and drop operation.
        }
      } else if (item.level > newPreviousItem.level) {
        // The moved item is lower (visually) is the hierarchy than the previous item.
        if (item.level == newPreviousItem.level + 1) {
          nestedItem.remove(modelsController);
          if (modelsController.items[newPreviousItem.key]!.isCollapsed) {
            // Move the item at the end of the new previous item.
            newPreviousNestedItem.children.add(nestedItem);
          } else {
            // Move the item at the beginning of the new previous item.
            newPreviousNestedItem.insert(0, nestedItem);
          }
          setState(() {});
        } else {
          if (modelsController.items[newPreviousItem.key]!.isCollapsed) {
            // Add it to the very end.
            try {
              newPreviousNestedItem.addNested(nestedItem);
              nestedItem.remove(modelsController);
              setState(() {});
            } on StateError {
              // It was not a valid drag and drop.
            }
          } else {
            // Bad drag and drop, do nothing.
          }
        }
      } else {
        // The moved item is higher (visually) is the hierarchy than the previous item.
        if (onReorderParam.newNextItem == null) {
          // The item is dragged at the end of the list.
          // This is always a valid drop.
          nestedItem.remove(modelsController);
          nestedItems.addNested(nestedItem);
          setState(() {});
        } else {
          final newNextItem = onReorderParam.newNextItem!;
          final newNextNestedItem = nestedItemsMap[newNextItem.key.value]!;
          if (modelsController.items[item.key]!.isCollapsed) {
            // The item is collapsed.
            // Move it at the end of its new parent.
            var previousRelative = newPreviousNestedItem;
            while (previousRelative.level > nestedItem.level) {
              previousRelative = previousRelative.parent!;
            }
            final parent = previousRelative.parent!;
            var newIndex = previousRelative.indexInParent + 1;
            if (nestedItem.parent == parent &&
                nestedItem.indexInParent <= newIndex) {
              // The item is currently before the previous relative item. Since
              // the item is going to be removed, 1 needs to be subtracted from
              // `newIndex`.
              newIndex--;
            }
            nestedItem.remove(modelsController);
            parent.insert(
              previousRelative.indexInParent + 1,
              nestedItem,
            );
            setState(() {});
          } else {
            if (newNextItem.level > item.level + 1) {
              // This is not a valid drop.
              return;
            } else if (newNextItem.level <= item.level) {
              // Leave the item where it is.
              nestedItem.remove(modelsController);
              final parent = newNextNestedItem.parent!;
              parent.children[newNextNestedItem.indexInParent - 1]
                  .addNested(nestedItem);
              setState(() {});
            } else {
              // The new next item should be added to item.
              try {
                nestedItem.remove(modelsController);
              } on StateError {
                // Not a valid drop.
                return;
              }
              final previousRelative = newNextNestedItem.parent!;
              final parent = previousRelative.parent!;
              parent.insert(
                previousRelative.indexInParent + 1,
                nestedItem,
              );

              final newNextNestedItemIndexInParent =
                  newNextNestedItem.indexInParent;
              final newPreviousRelativeChildren =
                  previousRelative.children.sublist(
                0,
                newNextNestedItemIndexInParent,
              );
              final newChildren = previousRelative.children.sublist(
                newNextNestedItemIndexInParent,
                previousRelative.children.length,
              );
              previousRelative.children
                ..clear()
                ..addAll(newPreviousRelativeChildren);
              nestedItem.children
                ..clear()
                ..addAll(newChildren);
              setState(() {});
            }
          }
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ExpandableReorderableList<ValueKey<String>>(
        leads: const [
          LeadOrTail(text: 'Header'),
        ],
        tails: const [
          LeadOrTail(text: 'Tail'),
        ],
        onReorder: onReorder,
        modelsController: modelsController,
        children: nestedItems.item.children,
      ),
    );
  }
}

/// An extension on [NestedItem] with helper methods.
extension on NestedItem {
  /// Remove an item from its parent and pass all its children to the previous
  /// item.
  void remove(
    ExpandableReorderableListItemModelController<ValueKey<String>>
        modelsController,
  ) {
    if (modelsController.items[item.key]!.isCollapsed || children.isEmpty) {
      // We move the children too.
      parent!.children.remove(this);
    } else {
      // Move only the header and not the children with it.
      final _indexInParent = indexInParent;
      if (_indexInParent == 0) {
        // We cannot move the children to the previous item, do nothing.
        throw StateError('Cannot remove the first item without its children.');
      }
      parent!.children[_indexInParent - 1].addAll(children);
      children.clear();
      parent!.children.remove(this);
    }
  }

  /// The associated item.
  ExpandableReorderableListItem<ValueKey<String>> get item {
    return ExpandableReorderableListItem<ValueKey<String>>(
      key: ValueKey<String>(name),
      children: children.map((child) => child.item).toList(),
      builder: (_, __, itemModel) => ItemWidget(
        name: name,
        level: level,
        itemModel: itemModel,
        hasChildren: children.isNotEmpty,
      ),
    );
  }

  /// Returns a map including the item and all its sub-items.
  Map<String, NestedItem> toMap() {
    final map = <String, NestedItem>{};
    map[name] = this;
    for (final child in children) {
      map.addAll(child.toMap());
    }
    return map;
  }

  /// Add recursively a new item to the list. If it is not possible, it will
  /// throw a [StateError].
  void addNested(NestedItem nestedItem) {
    assert(nestedItem.level > level);
    if (nestedItem.level == level + 1) {
      add(nestedItem);
    } else {
      if (children.isEmpty) throw StateError('No children');
      children.last.addNested(nestedItem);
    }
  }
}
38
likes
160
pub points
81%
popularity

Publisher

verified publishernovade.net

A wrapper around ReorderableListView that allows you to expand and collapse items.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

equatable, flutter, visibility_detector

More

Packages that depend on expandable_reorderable_list