Fridge Poetry in Flutter
or, an example program using Draggables and DragTargets
For my next Flutter app I thought it would be useful to learn how to drag and drop Widgets into different areas of the app. To do this, I'm going to recreate the fridge poetry magnet sets you might have seen. The app will show multiple words in little boxes that can be dragged around to create Pulitzer Prize-winning poems.
The Magnet Widget
First off, let's create a new app and clear out all the example code. Then create a new class that extends StatelessWidget:
class Magnet extends StatelessWidget {
final String word;
const Magnet(this.word, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Draggable(
data: word,
feedback: Transform.rotate(angle: -0.1, child: _createMagnet()),
child: _createMagnet(),
childWhenDragging: Container(),
);
}
Widget _createMagnet() {
return Container(
width: 100,
height: 30,
child: Center(
child: Text(
word,
style: const TextStyle(
fontSize: 14,
color: Colors.black,
),
),
),
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
color: Colors.white,
),
);
}
}
The _createMagnet
method is straightforward. I'm just creating a Container
with a fixed size and border that has some word displayed in it.
The build
method is a bit more interesting. Here I'm returning a Draggable
widget. This has few things to note. First we have our data
. This is any data that needs to be passed to the DragTarget
when the dragging stops. This can be used to determine whether the drag succeeds or is invalid, but for our use case here all that's required is to include the word we're dragging.
Next up is child
, which is how the Widget is displayed when it's not being dragged. Here I'm just returning the returned value of _createMagnet
.
Next is feedback
, which is what to display while dragging, under the user's cursor or finger. I want it to be obvious the user is interacting with the magnet, so I'm wrapping the result of _createMagnet
in a Transform.rotate
so there's a bit of a tilt to it.
Lastly is the childWhenDragging
. This is what to display in the start location while the Widget is being dragged. Here I want the magnet to move every time it's dragged, so it doesn't make sense to show anything while the user is dragging. Instead, I'm returning an empty Container
.
Meanwhile, in main.dart...
Back in main.dart
I need to track some data and use my new Magnet
widget. For the data, I created another class, cleverly named MagnetInfo
to track my magnet's word and position:
class MagnetInfo {
double x;
double y;
String word;
MagnetInfo(this.x, this.y, this.word);
}
I also created a list of MagnetInfo with some dummy info:
List<MagnetInfo> _magnets = [
MagnetInfo(10, 10, "hello"),
MagnetInfo(30, 30, "world"),
MagnetInfo(50, 50, "drink"),
MagnetInfo(70, 70, "coffee"),
...
}
Now I need a spot for my magnets to live: the Fridge
!
class Fridge extends StatelessWidget {
const Fridge({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: DragTarget(
builder: (context, candidateItems, rejectedItems) {
return Stack(
children: _magnets.map((MagnetInfo magnet) {
return Positioned(
child: Magnet(magnet.word),
left: magnet.x,
top: magnet.y,
);
}).toList(),
);
},
onMove: (onMove) {
var magnet =
_magnets.firstWhere((magnet) => magnet.word == onMove.data);
magnet.x = onMove.offset.dx;
magnet.y = onMove.offset.dy;
},
),
);
}
}
For the widgets to be able to be displayed anywhere (something like the CSS absolute position, for you web devs out there), I used a Stack
, which lets me define a left and top coordinate for the Widget.
For my draggables to actually do something when they're dragged, I need a DragTarget
. Here, the DragTarget
wraps my Stack
so anything moving on the screen is technically in bounds. For this app I'm cheating just a bit by using the onMove
method to update my global _magnets
list for the word I'm dragging, and update the x and y to match the new offset.
Draggables
can be a lot more interesting than my example, however. You'll notice the builder
method above, that I only use to return the Stack
. It has candidateItems
and rejectedItems
that contain the data
of the draggable (the word, for our magnets). We could add some logic there to do something special if the drag is accepted or not. We can decide whether to accept or deny using the DragTarget
's onWillAccept
method.
However, this is a minimal example that doesn't require that functionality. All we really want is to drag some words around and hopefully create some poems. Check out the repo here.
Resources
- https://medium.com/flutter-community/a-deep-dive-into-draggable-and-dragtarget-in-flutter-487919f6f1e4#:~:text=While%20a%20Draggable%20allows%20a,is%20accepted%20by%20the%20DragTarget%20.
- https://docs.flutter.dev/cookbook/effects/drag-a-widget