baumths / flutter_tree_view

A Flutter collection of widgets and slivers that helps bringing your hierarchical data to life.
https://baumths.github.io/flutter_tree_view
MIT License
175 stars 62 forks source link

Re-ordering and Moving Nodes #62

Closed JerContact closed 11 months ago

JerContact commented 1 year ago

I'm implementing a drag and drop, and the treeController won't update properly when moving around nodes, is there a way to reboot the treeController to just rebuild everything? All the data looks right in the treeMap, but when the treeController builds, it just doesn't seem the adjust nodes for some reason.

This happens a lot when having nodes expanded and then dragging that parent around and asking the treeController to update.

baumths commented 1 year ago

Hey @JerContact

Could you provide a small reproducible example?

JerContact commented 1 year ago

it would be very complex to try and reproduce code for this, since it's using bloc and it's fairly imbedded inside a huge project right now.

But my question is really about how can i just reset the treeController so it's not just looking for listener changes, but literally just rebuilds everything from scratch according to the treeMap

baumths commented 1 year ago

The TreeController doesn't hold anything, so calling TreeController.rebuild() should always update the trees. If they aren't updating you might be facing another issue. Have you tried giving your node widgets a key?

JerContact commented 1 year ago

Each node has a key for my treeMap, let's say i have this

TreeMap has 2 entries [0] - root nodes with 4 items [1] - child with key 16 (the id of the 4th root node) and there are 2 items in this

I then totally replace the root nodes with a re-ordered version, everything else stays the same (the child still remains in the treemap)

when the treecontroller rebuilds it only rebuilds the roots and doesn't seem to pay attention to the children, even though they are in the treeMap

JerContact commented 1 year ago

It's weird, in the node builder, it builds less nodes as well, and if I move the roots back into their old order by replacing the root nodes again, it all appears again....

baumths commented 1 year ago

I'm afraid I may not be able to help you without looking at the source code.

I have no clue of what the issue might be.

JerContact commented 1 year ago

I believe this is a similar example, just need to replace "lazy_loading.dart" in your example project with this

import 'dart:math' show Random;

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

import '../shared.dart' show watchAnimationDurationSetting;

class Data {
  static const Data root = Data._root();

  const Data._root()
      : id = 0,
        title = '/';

  // static int _uniqueId = 1;
  Data(this.title, this.id);

  final int id;
  final String title;
}

int uniqueId = 4;

int _generateUniqueId() => uniqueId++;

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

  @override
  State<LazyLoadingTreeView> createState() => _LazyLoadingTreeViewState();
}

class _LazyLoadingTreeViewState extends State<LazyLoadingTreeView> {
  late final Random rng = Random();
  late final TreeController<Data> treeController;

  Iterable<Data> childrenProvider(Data data) {
    return childrenMap[data.id] ?? const Iterable.empty();
  }

  final Map<int, List<Data>> childrenMap = {
    Data.root.id: [Data('One', 1), Data('Two', 2), Data('Three', 3)],
  };

  final Set<int> loadingIds = {};

  Future<void> loadChildren(Data data) async {
    final List<Data>? children = childrenMap[data.id];
    if (children != null) return;

    setState(() {
      loadingIds.add(data.id);
    });

    await Future.delayed(const Duration(milliseconds: 750));

    childrenMap[data.id] = List.generate(
      rng.nextInt(4) + rng.nextInt(1),
      (_) => Data('Node', _generateUniqueId()),
    );

    loadingIds.remove(data.id);
    if (mounted) setState(() {});

    treeController.expand(data);
  }

  Widget getLeadingFor(Data data) {
    if (loadingIds.contains(data.id)) {
      return const Center(
        child: SizedBox.square(
          dimension: 20,
          child: CircularProgressIndicator(strokeWidth: 2),
        ),
      );
    }

    late final VoidCallback? onPressed;
    late final bool? isOpen;

    final List<Data>? children = childrenMap[data.id];

    if (children == null) {
      isOpen = false;
      onPressed = () => loadChildren(data);
    } else if (children.isEmpty) {
      isOpen = null;
      onPressed = null;
    } else {
      //isOpen = treeController.getExpansionState(data);
      isOpen = true;
      onPressed = () => treeController.toggleExpansion(data);
    }

    return FolderButton(
      key: GlobalObjectKey(data.id),
      isOpen: isOpen,
      onPressed: onPressed,
    );
  }

  @override
  void initState() {
    super.initState();
    treeController = TreeController<Data>(
      roots: childrenProvider(Data.root),
      childrenProvider: childrenProvider,
    );
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: AnimatedTreeView<Data>(
            treeController: treeController,
            nodeBuilder: (_, TreeEntry<Data> entry) {
              return TreeIndentation(
                entry: entry,
                child: Row(
                  children: [
                    SizedBox.square(
                      dimension: 40,
                      child: getLeadingFor(entry.node),
                    ),
                    Text(entry.node.title),
                  ],
                ),
              );
            },
            duration: watchAnimationDurationSetting(context),
          ),
        ),
        ElevatedButton(
            onPressed: () {
              setState(() {
                final test = [
                  Data('Three', 3),
                  Data('One', 1),
                  Data('Two', 2),
                ];
                treeController.roots = test;
              });
            },
            child: const Text('Change Order')),
      ],
    );
  }
}

To Repro:

  1. Go into lazy loading
  2. Press one of the folders and get nodes back
  3. press the button at the bottom "Change Order"
  4. Notice the nodes are open, the childrenMap is correct, but the children of the folder don't appear
baumths commented 1 year ago

Just looking at the example, I noticed that you are creating new Data instances with the same id, but the Data class doesn't override the == operator... the TreeController uses a Set data structure to store the expansion state of tree nodes. Without overriding the == operator, those nodes would not match (Set.contains would return false even though the ids match), loosing the expansion state between rebuilds.

It's pretty late here. I will take a deeper look at this tomorrow. Let me know if overriding the == operator solves your issue.

JerContact commented 1 year ago

Ahhhhh, the == does seem to fix this issue on the example, let me try my project and get back to you, thanks for getting back!!

JerContact commented 1 year ago

i guess my problem is i'm essentially replacing the whole tree since it can get really complicated when moving multiple nodes around from all over the tree view, so I think it's literally breaking the functionality of the treeController....is there a way the treeController could just start from scratch each time a rebuild happens? But, keep it's heirachy and which nodes are open and connected to each other?

I might have to just fork a change to do it, but any help to guide me where i would make these changes would be super helpful!

baumths commented 11 months ago

Hey, sorry for the delayed response.

I'm pretty sure just setting treeController.roots = newRoots should do the trick, as long as your tree nodes have consistent == and hashCode implementations so you keep the expansion state upon tree refresh.

Keeping the nodes hierarchy and relationships is up to you as there are many ways to represent a tree and I didn't want to force all users to depend on a mandatory implementation.

I'll close this issue for now but feel free to reopen it. :blush: