hanshengchiu / reorderables

Reorderable table, row, column, wrap, and sliver list that allow drag and drop of the children. https://pub.dartlang.org/packages/reorderables
MIT License
740 stars 170 forks source link

How do I keep the state of my widget after reordering it? #72

Open Levaru opened 4 years ago

Levaru commented 4 years ago

Here is my stackoverflow question where I explain everything in detail: Link

INotDisposable commented 4 years ago

That's really weird - I would also expect StatefulWidget to maintain its state during and after the dragging. I'm using the 'get' package these days to keep UI state and UI drawing completely separate, but the behavior you describe is pretty surprising. Thanks for the simple StatefulWidget sample in the stackoverflow post - I'm going to take a look at that, reproduce, and then see if I can figure out what's going on.

I'm about to use the 'reorderables' package in my own app, so digging in and really understanding it will be a good exercise. :)

INotDisposable commented 4 years ago

Okay, so what's happening is that the TestWidget is being removed and re-added to the widget tree when a drag event starts. When this happens, createState on the TestWidget being dragged gets called and creates a new State object with the default blue color. The comment in the code around toWrap is correct, a global key is needed to preserve state. However, where the global key is really needed is in the original stateful widget (TestWidget) and not in the wrapped widgets that the reorderables code creates. Unfortunately you can't make your own widget's key a GlobalKey because then you get duplicate key exceptions when the reorderable container is rendered.

Here's the relevant section from the StatefulWidget docs (https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html):

A StatefulWidget keeps the same State object when moving from one location in the tree to another if its creator used a GlobalKey for its key. Because a widget with a GlobalKey can be used in at most one location in the tree, a widget that uses a GlobalKey has at most one associated element.

One possible fix for this would be to require that all widgets added to a reorderable use global keys. This would require a documentation update and a change to the reorderables package to avoid duplicate global key exceptions.

The other option is to maintain state outside of the State object like what you've done in the Stack Overflow post. It's all we've got thanks to how Flutter works when an element is removed from and re-added to the tree. Also relevant from the StatefulWidget docs:

Similarly, if a StatefulWidget is removed from the tree and later inserted in to the tree again, the framework will call createState again to create a fresh State object, simplifying the lifecycle of State objects.

It might simplify the lifecycle, but it screws up what you would expect to happen when something is dragged in a reorderable. 🥺

A simple solution for maintaining state outside of State but still within the StatefulWidget is to add a Map to the widget and then reference that from the state.

The modified code for your sample StatefulWidget:

class TestWidget extends StatefulWidget{
  final Map<String,dynamic> state = Map<String,dynamic>();

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

Adding a getter and setter to the State that references the map in its parent widget:

class _TestWidgetState extends State<TestWidget> {
    Color get boxColor {
      return widget.state["boxColor"] ?? Colors.blue;
    }
    set boxColor(Color color) {
      widget.state["boxColor"] = color;
    }

    @override
    void initState() {
      super.initState();
      // If you want to preserve state across calls to createState(), don't
      // initialize the setter here.  Use a default value like above.
    }
.
.
.
}

It feels extremely like a Flutter anti-pattern to do this, but without being able to use a GlobalKey for our own widget it's the only other option I think.

As I mentioned before, I'm using the get package to maintain state in controllers, and then using GetBuilder<T> widgets to draw things using that controller state. Because of how things are architected when state is separated like this, the problem being discussed here is avoided completely. I think the same would go for any other state management package like MobX.

Sorry for the book of text - it was interesting to dig into and figure out. Incidentally, others have also asked about the underlying issue that's causing this: https://stackoverflow.com/questions/59516450/preserve-widget-state-when-temporarily-removed-from-tree-in-flutter

doppio commented 4 years ago

This seems directly related to a bug in the official Flutter ReorderableListView: https://github.com/flutter/flutter/issues/58364

renancaraujo commented 3 years ago

I tried to fix that with the same approach as flutter/flutter#58364 was fixed with no success, internally there are so many commented code and so many anti-patterns (like saving widget instances on the state) that it was a risky move to keep going.

danReynolds commented 2 years ago

Made a PR that supports a GlobalKey for children here if folks are still running into this issue: https://github.com/hanshengchiu/reorderables/pull/164. Whether or not we end up merging this it's good to know if other folks want to fork and solve the problem this way.

cavator commented 1 year ago

@danReynolds i have no idea what your PR fixed, because i still lost my states when dragging. can u provide some code sample? cause here still losing the state, i'm building a image picker + order-able list like tinder image picker, my img vanish when i drag, and still vanished when i let it