Update an deeply nested array in a freezed class in flutter
Asked Answered
C

4

10

I have a freezed class in flutter as follows:

@freezed
abstract class Data with _$Data {
  const factory Data({
    String id,
    String name,
    String parentId,//null if it is the root element
    @Default([]) List<Data> children,
  }) = _Data;
}

The class contains a property called children which is a list of the same class i.e Data.

The max nesting that is currently allowed 20 levels deep. The problem I am facing is how to update a particular deeply nested children list by adding or removing items to it. Also this updating should be done keeping immutability and return a new updated Data class.

I tried using copyWith() method on the freezed class but could not figure out the same when there is deeply nested as in my scenario.

Cud answered 11/6, 2020 at 15:0 Comment(0)
W
3

I think it's not enough info to write a precise answer. But I will try to show an idea of how to do it.

Here is the code:

/// recursive func to update children (should not be used directly, use `updateChildren` instead)
Data updateChildrenAt(Data data, List<Data> Function(List<Data>) update,
    List<int> indices, int depth) {
  final children = data.children;
  List<Data> newChildren;

  if (depth < indices.length) {
    final index = indices[depth];
    final child = children[index];
    newChildren = children.toList();
    newChildren[index] = updateChildrenAt(child, update, indices, depth + 1);
  } else {
    newChildren = update(children);
  }
  return data.copyWith(children: newChildren);
}

/// func to update children in the tree
Data updateChildren(
    Data data, List<Data> Function(List<Data>) update, List<int> indices) {
  return updateChildrenAt(data, update, indices, 0);
}

It is a simple recursive function (you should be aware of the fact, that if the depth is too deep it would fail with an exception, but depth like 20 is OK) that searches for children list to update by provided path (indices), because I don't know exactly what info you have and decided to write some kind of a generic algo (I hope, so...).

And here is an example how to use it:

import 'data.dart';

/// func for demonstration
Data data(String name, [List<Data> children]) {
  return Data(name: name, children: children ?? []);
}

/// func for demonstration
String dataToStr(Data data, [int level = 0]) {
  var str = ' ' * level + data.name + ':\n';

  for (var i = 0; i < data.children.length; ++i) {
    str += dataToStr(data.children[i], level + 1);
  }

  return str;
}

/// func for demonstration
void printData(Data data) {
  print(dataToStr(data, 0));
}

void main(List<String> arguments) {
  final r = data('root', [
    data('level 1', [
      data('level 2_1', [
        data('level 3', [
          data('level 4', [
            data('level 5_1'),
            data('level 5_2',
                [data('level 6_1'), data('level 6_2'), data('level 6_3')]),
            data('level 5_3')
          ])
        ])
      ]),
      data('level_2_2'),
    ]),
  ]);

  // print initial value
  printData(r);
  // root:
  // level 1:
  //  level 2_1:
  //   level 3:
  //    level 4:
  //     level 5_1:
  //     level 5_2:
  //      level 6_1:
  //      level 6_2:
  //      level 6_3:
  //     level 5_3:
  //  level_2_2:

  // removing first element and adding new element at the end deep in the tree
  printData(updateChildren(r, (List<Data> children) {
    return children.sublist(1, children.length)..add(data('level 7 (added)'));
  }, [0, 0, 0, 0, 1]));
  // root:
  // level 1:
  //  level 2_1:
  //   level 3:
  //    level 4:
  //     level 5_1:
  //     level 5_2:
  //      level 6_2:
  //      level 6_3:
  //      level 7 (added):
  //     level 5_3:
  //  level_2_2:

  // adding new element in the start and in the end not very deep in the tree
  printData(updateChildren(r, (List<Data> children) {
    return [data('level 1_0 (added)'), ...children, data('level 1_2 (added)')];
  }, []));
  // root:
  // level 1_0 (added):
  // level 1:
  //  level 2_1:
  //   level 3:
  //    level 4:
  //     level 5_1:
  //     level 5_2:
  //      level 6_1:
  //      level 6_2:
  //      level 6_3:
  //     level 5_3:
  //  level_2_2:
  // level 1_2 (added):
}

You should be aware of the fact that all checks have been omitted for simplicity (I think it's not very difficult to add them, btw).

Upd: What are indices in this example?

As I said due to lack of precise info (or maybe I haven't understood it well enough) I've decided to use indices.

The problem is before the update a list of children we are to find that list (the list we want to update). We can't find it only with info about the depth because the specified data represents a tree-like data structure. Let's look at an example:

                        root
                          |
0                     [level 1 ]
                          |
1                    [level 2_1, level 2_2]
                          |          |
2                    [level 3]      [ ]
                          |
3                    [level 4]
                          |
4 [level 5_1,         level 5_2,             level 5_3]
       |                  |                      |
5     [ ]   [level 6_1, level 6_2, level 6_3]   [ ]

It's a representation of a root object from an example code (simplified, I hope I've made it right...). If we want (like in the first example) to update a list of [level 6_1, level 6_2, level 6_3] info about depth is not enough. Because on the same depth there are 3 lists. What exactly list we want to update? That's why I've added indices. The numbers 0-5 mean the required indices list length on specified depth. And index in this list means what exactly list we want to update on specified depth (because, as you can see on the image on every depth could be any number of lists). In the first example, we are updating [level 6_1, level 6_2, level 6_3] list. And indices just like specify a precise path to it. So [0, 0, 0, 0, 1] means that we want to update root.children[0].children[0].children[0].children[0].children[1].children list. If we want, for example, to update an empty list under level 2_2 we should be able to do it with indices: [0, 1]. In the last example, we specified an empty indices, that's mean we want to update the root children list, i.e. [level 1] list.

Because you haven't specified enough info, I couldn't say is this the code you really want. Maybe you should provide a bit more info about a task you want to accomplish. For example, maybe you already have a list that you want to update, in that case, the code above would not work without modification. Another example, maybe you want to update all lists on a specified depth. In that case, we need to change code either. Or maybe a data structure is wrong (for example), and should be changed. I hope it's a bit clearer now.

Whipperin answered 20/6, 2020 at 15:0 Comment(2)
Thank you very much. I really appreciate your presentation of the answer. But could you explain how exactly the indices parameter is used?Cud
thanks for the detailed explanation. I can modify to make it work for my needs. Also another I found of doing the same is flattening out the data into an array, then updating this flattened array, then unflatten it back. What do you think of that?Cud
D
2

You can combine copyWith with the spread operator (...) to clone the list.

Assuming you have:

abstract class Data with _$Data {
  const factory Data({
    String name,
    @Default([]) List<Data> children,
  }) = _Data;
}


var root = Data(
  name: 'root',
  children: [
    Data(name: 'first'),
  ],
)

You could clone the tree and add a child to root by doing:

root = root.copyWith(children: [
  ...root.children,
  Data(name: 'second'),
])
Dolphin answered 11/6, 2020 at 18:15 Comment(3)
but how can it be done for children property of a Data that is n levels deep? Do we have to use some sort of recursion? I could not wrap my head around it.Cud
Yeah you'll need a recursionShopworn
If you don't mind, could you provide some pseudo code of how it can be done with recurrsionCud
I
0

Inspired by flutter_tree_view

import 'package:freezed_annotation/freezed_annotation.dart';

part 'sealed_tree_node.freezed.dart';

@freezed
class SealedTreeNode<T> with _$SealedTreeNode<T> {
  const SealedTreeNode._();

  factory SealedTreeNode(
      {required String id,
      @Default('') String label,
      required T data,
      String? parentId,
      @Default([]) List<SealedTreeNode<T>> children}) = _SealedTreeNode<T>;

  SealedTreeNode<T> operator [](int index) => children.elementAt(index);

  bool get hasChildren => children.isNotEmpty;

  bool hasChild(SealedTreeNode<T> child) {
    return children.contains(child);
  }

  Iterable<SealedTreeNode<T>> get descendants sync* {
    for (final child in children) {
      yield child;

      if (child.hasChildren) {
        yield* child.descendants;
      }
    }
  }

  Iterable<SealedTreeNode<T>?> get nullableDescendants sync* {
    for (final child in children) {
      yield child;
      if (child.hasChildren) {
        yield* child.nullableDescendants;
      }
    }
  }

  SealedTreeNode<T>? getParent(SealedTreeNode<T> root) {
    if (parentId != null) {
      return root.nullableDescendants
          .firstWhere((element) => element!.id == parentId, orElse: () => null);
    }
  }

  Iterable<SealedTreeNode<T>> getAncestors(SealedTreeNode<T> root) sync* {
    final parent = getParent(root);
    if (parent != null) {
      yield* parent.getAncestors(root);
      yield parent;
    }
  }

  SealedTreeNode<T> removeNestedChild(SealedTreeNode<T> childToRemove,
      {SealedTreeNode<T>? root}) {
    return (root ?? this).copyWith(
        children: (root ?? this)
            .children
            .where((child) => child != childToRemove)
            .map((child) => removeNestedChild(childToRemove, root: child))
            .toList());
  }

  SealedTreeNode<T> updateNestedChild(SealedTreeNode<T> childToUpdate, T data,
      {SealedTreeNode<T>? root}) {
    return (root ?? this).copyWith(children: [
      ...(root ?? this)
          .children
          .where((child) => child != childToUpdate)
          .map((child) => updateNestedChild(childToUpdate, data, root: child))
          .toList(),
      ...(root ?? this)
          .children
          .where((child) => child == childToUpdate)
          .map((child) => child.copyWith(data: data))
          .toList()
    ]);
  }

  SealedTreeNode<T>? find(String id) => nullableDescendants.firstWhere(
        (descendant) => descendant == null ? false : descendant.id == id,
        orElse: () => null,
      );

  @override
  String toString() => 'TreeNode(id: $id, label: $label)';

  String showTree() {
    return showSubTree(this, 0);
  }

  static String showSubTree(SealedTreeNode node, int indent) {
    var treePrint = '';
    final indentString = '  ' * indent;
    final branchString = '-' * indent;
    if (indent > 0) {
      treePrint += '\n$indentString|$branchString${node.label}[${node.id}]';
    } else {
      treePrint += '\n${node.label}[${node.id}]';
    }
    for (final childNode in node.children) {
      treePrint += showSubTree(childNode, indent + 1);
    }
    return treePrint;
  }
}
Inter answered 9/9, 2021 at 20:57 Comment(1)
Please add further details to expand on your answer, such as working code or documentation citations.Odiliaodille
M
-1

Use @unfreezed adnotation to have mutable elements

Martino answered 5/8, 2022 at 8:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.