Greg's blog

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

#flutter