Search code examples
flutterdraggableproviderconsumerdragtarget

Flutter Draggable - How to Reset Draggable to Starting Position?


Updates:

See latest updates at the bottom.

Original

I have flutter Draggable and DragTarget widgets that are working except that after I place the DragTarget I get to a particular situation where I want to rebuild the DragTarget and move the Draggable back to its original position.

When I check the Flutter inspector it shows my widgets where I expect them to be but when I call my Provider for the next set of information, the Draggables remain in their dropped position rather than returning to their starting position.

As seen in the last frame of the image, the three target frames still contain a reference to the draggable widgets but I am rebuilding both the Draggable and DragTarget widgets on each click of the red IconButton.

Observations

  • Draggables remain in dropped position even though I am wrapping the Tiles in the parent widget with a Consumer.
  • The Consumer is passing the changed data values.
  • The DragTargets are visible in my widget tree using Flutter
    Inspector.
  • When I get to an event where I have more than three Draggables, the
    dropped draggables are misplaced while the Tiles and DropTargets for the other widgets appear as expected.

Expectations:

When I reset my consumer data I want to have both Tiles and Targets redrawn in their original positions with the new data.

draggable_behavior

The DragTarget

@override
  Widget build(BuildContext context) {
    return DragTarget(
      builder:
          (context, List<String> candidateData, List<dynamic> rejectedData) {
        return (isSuccessful) //&& !widget.isPuzzleComplete
            ? Tile(
                title: widget.data,
              )
            : TargetPlaceholder(
                widget: widget,
              );
      },
      onWillAccept: (data) {
        return widget.data == data;
      },
      onAccept: (data) {
        setState(() {
          Provider.of<GameController>(
            context,
            listen: false,
          ).checkPuzzleSolved();
          //activate the animation, may not require setState
          isSuccessful = !widget.isPuzzleComplete;
        });
      },
      onLeave: (data) {
        print('Draggable object left the target area containing data of $data');
      },
    );
  }
}

Draggable

@override
  Widget build(BuildContext context) {
    return Draggable(
      data: widget.title,
//      dragAnchor: DragAnchor.pointer,
      child: isSuccessful ? Container() : TileContent(widget: widget),
      childWhenDragging: Container(),
      feedback: Opacity(
        opacity: 0.7,
        child: TileContent(
          widget: widget,
        ),
      ),
      onDragCompleted: () {
        setState(() {
          Transform(
              transform: Matrix4.translation(_shake()),
              child: TileContent(widget: widget));
        });
      },
      onDragEnd: (details) {
        setState(() {
          isSuccessful = details.wasAccepted;
        });
      },
    );
  }
}

Parent Widget

 Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              for (var item in targetPattern)
                Consumer<GameController>(builder: (context, gc, _) {
                  return Target(
                    data: item,
                    isPuzzleComplete: gc.isPuzzleComplete,
                  );
                })
            ],
          ),
          SizedBox(
            height: 36,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              for (var data in scrambledPattern)
                Consumer<GameController>(builder: (context, gc, _) {
                  return Tile(
                    title: data,
                    isPuzzleComplete: gc.isPuzzleComplete,
                  );
                })
            ],
          ),

Updated Behavior

See the note below and the second animated gif to compare and contrast the code changes and new behavior with the original behavior above.

After adding a UniqueKey to both the Targets and Tiles - The Tiles are incorrectly shown in their original location, The Targets are correctly shown.

What I am seeking to do is:

  • Have the Tiles shown in their dropped position on the Target
  • After a different click event have the Tiles and Targets redrawn in their original positions.

Note - Applied the following code to both the Tile and Target widget.

key: (gc.isPuzzleComplete == true) ? UniqueKey() : null,

uniquekey-update

The Solution

Thanks to @LoVe's reminder on using UniqueKey and a review of the Flutter Team's video on Keys; I was able to resolve the issue by conditionally adding the UniqueKey to the enclosing Row containing both the Tiles and the Target.

From within each of these widgets I was able to update the appearance of the widgets using (isSuccessful || widget.isPuzzleComplete). I was also able to remove the Consumer logic related to grabbing the isPuzzleComplete logic from the Tile/Target widgets and from the setState call.

Currently, the behavior is 100% correct.

//mod to parent row of Tiles
Row(
  key: widget.isPuzzleComplete ? UniqueKey() : null,
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: <Widget>[
   for (var data in scrambledPattern)
     Tile( title: data,
           isPuzzleComplete: widget.isPuzzleComplete,
          ), 
        ],
      ),

//mod to draggable Tile
Draggable(
  key: (widget.isPuzzleComplete == true) ? UniqueKey() : null,
  data: widget.title,
  child: (isSuccessful || widget.isPuzzleComplete)
      ? Container()
      : TileContent(widget: widget),

//mod to drag target 
return DragTarget(
      key: UniqueKey(),
      builder:
          (context, List<String> candidateData, List<dynamic> rejectedData) {
        return (widget.isPuzzleComplete || isSuccessful)
            ? Tile(
                title: widget.data,

References

Flutter Team Video on use of Keys (link)


Solution

  • You will have to use keys, something like this:

    Tile( key:UniqueKey(), title: data, isPuzzleComplete: gc.isPuzzleComplete, ); }
    

    Whenever you are using a Row or Column or List of widgets of the same type and you are updating them frequently, then you must use Keys so the framework knows when to and when to not update the widgets of the same type,

    more about using keys here