DEV Community

Cover image for Flutter Drag and Drop Example
Bart van Wezel
Bart van Wezel

Posted on • Originally published at bartvwezel.nl on

Flutter Drag and Drop Example

For a little game, I needed to drag and drop items. I have never seen it as easy as in Flutter. So in this blog post, we will describe how to implement the drag and drop of checkers on checkerboard in Flutter. The goal is to draw a checkerboard, add checkers, and make it possible for the checkers to be dragged around. For state management, we will use the provider. If this is completely new to you, the previous blog post explaining the Provider might be useful as won’t cover it completely in this post.

Setup the project

Before we can start with coding, we are going to add a dependency to the project, namely provider. Their package page contains a lot of useful information and references to more use cases and examples

dependencies:
  provider: ^4.3.2+3
Enter fullscreen mode Exit fullscreen mode

Do not forget to install the dependency, running the following command:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

That is it! We can now start with rewriting the example.

Drawing a checkerboard

Since we are going to show how drag and drop work on a checkboard, so let’s start by drawing a checkerboard. We have already shown how to draw a hexagonal grid, but luckily for the checkerboard, this is much simpler. Flutter provides a GridView, which we can utilize to draw the checkerboard.

class CheckerBoard extends StatefulWidget {
  @override
  _CheckerBoardState createState() => _CheckerBoardState();
}

class _CheckerBoardState extends State {
  final List squares = [];

  @override
  void initState() {
    super.initState();
    for (var x = 0; x < 8; x++) {
      for (var y = 0; y < 8; y++) {
        squares.add(new Square(x: x, y: y));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return GridView.count(
        physics: new NeverScrollableScrollPhysics(),
        padding: const EdgeInsets.all(10),
        crossAxisCount: 8,
        children: squares);
  }
}

class Square extends StatelessWidget {
  final int x;
  final int y;

  Color getColor() {
    if (x % 2 == y % 2) {
      return Colors.grey[800];
    }
    return Colors.grey[100];
  }

  const Square({Key key, this.x, this.y}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      color: getColor(),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

The square returns the correct color, based on the position on the board. In the _CheckerBoardState we initialise the squares and provide them to the GridView. The GridView will put all the squares in a Grid and now we have a checkerboard!

The checkerboard after part one!

Adding Draggable Checkers

We can now add some checkers that we can drag around the board. The Checker Widget should return a Draggable Widget. For the Draggable Widget, we will have to return two things, the feedback, and the child. The child is what you see before you start dragging around. The feedback is what we are dragging around.

class Checker extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Draggable(
      feedback: Container(
        child: Icon(
          Icons.circle,
          color: Colors.red,
          size: 35,
        ),
      ),
      child: Container(
        child: Icon(
          Icons.circle,
          color: Colors.blue,
          size: 35,
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

For now we will simply add the Widget to the screen by appending the Checker to the list that is displayed by the GridView. For this we change the list of Squares in a list of Widgets and append the Checker to the list.

  final List widgets = [];
  @override
  void initState() {
    super.initState();
    for (var x = 0; x < 8; x++) {
      for (var y = 0; y < 8; y++) {
        widgets.add(new Square(x: x, y: y));
      }
    }
    widgets.add(Checker());
  }
Enter fullscreen mode Exit fullscreen mode

We can now move around the checker, but we cannot place it anywhere yet. If you want to play around with the code so far, you can do so on this Dartpad.

Dropping the Checkers on the Squares

For the checkers to be droppable on the squares, we need another part of the Flutter Library, namely the DragTarget. As the name explains, Draggables can be dragged on the DragTargets. Before we start with changing the Squares in DragTargets, we are going to change some of the state management, to keep the application simple. As you could see at the setup, we added the provider dependency. We will be using the Provider to do state management. If this is completely new to you, the previous blog post explaining the Provider might be useful as we won't go in-depth here.

The Board class will maintain the state of the board. Here we are interested in multiple things.

  • Squares
  • Checkers
  • Position on the Checkers

We support three operations that change the state:

  • Start picking up a checker
  • Dropping the checker on a square
  • Canceling dragging the checker (i.e. dropping it outside of the checkerboard)
class Board extends ChangeNotifier {
  List _state = [];
  List _grid = [];
  List _checkers = [];

  Board() {
    int id = 0;
    for (var x = 0; x < 8; x++) {
      for (var y = 0; y < 8; y++) {
        var tile = new Square(x: x, y: y, id: id);
        _grid.add(tile);
        id++;
      }
    }
    _checkers.add(Checker(id: 1));
    _state.add(PositionsOnBoard(1, 12));
  }

  List grid() => _grid.toList();

  Checker getCurrent(int gridId) {
    var position = _state.firstWhere((element) => element.squareId == gridId,
        orElse: () => null);
    if (position == null || position.dragged) {
      return null;
    }
    return _checkers.firstWhere((element) => element.id == position.checkerId);
  }

  startMove(int id) {
    _state.firstWhere((element) => element.checkerId == id).dragged = true;
    notifyListeners();
  }

  cancelMove(int id) {
    _state.firstWhere((element) => element.checkerId == id).dragged = false;
    notifyListeners();
  }

  finishMove(int id, int to) {
    _state.firstWhere((element) => element.checkerId == id).dragged = false;
    _state.firstWhere((element) => element.checkerId == id).squareId = to;
    notifyListeners();
  }
}

class PositionsOnBoard {
  int checkerId;
  int squareId;
  bool dragged = false;

  PositionsOnBoard(int checkerId, int gridId) {
    this.checkerId = checkerId;
    this.squareId = gridId;
  }
}

Enter fullscreen mode Exit fullscreen mode

So we can now extend our Draggable Checker. We provide it with an id so that the state can keep track of it. The Draggable has an option for data. This is what the DragTarget will receive when we try to drop it. We will also call the board and update it when the checker is starting to move and when the move is canceled.

class Checker extends StatelessWidget {
  final int id;

  const Checker({Key key, this.id}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, state, child) {
      return Draggable(
        data: id,
        onDragStarted: () {
          state.startMove(id);
        },
        onDraggableCanceled: (a, b) {
          state.cancelMove(id);
        },
        feedback: Container(
          child: Icon(
            Icons.circle,
            color: Colors.brown[300],
            size: 35,
          ),
        ),
        child: Container(
          child: Icon(
            Icons.circle,
            color: Colors.brown[300],
            size: 35,
          ),
        ),
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now extend the Square and wrap it with a DragTarget. This makes it possible for Draggables to be dropped on the target. For the drawing of the square, we will ask the state if there is a checker on the current position. If it is, then we will draw the checker there. Two functions we need to implement to complete the example are the onWillAccept and the onAccept. The onWillAccept should return a boolean whether or not the checker can be dropped there. In our case, we can drop the current checker back on the square or we can drop a checker on an empty square. Finally the onAccept function, here we handle the dropping of the checker. We are asking the state to finish the movement of the checker.

class Square extends StatelessWidget {
  final int x;
  final int y;
  final int id;

  const Square({Key key, this.x, this.y, this.id}) : super(key: key);

  Color getColor() {
    if (x % 2 == y % 2) {
      return Colors.grey[800];
    }
    return Colors.grey[100];
  }

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, state, child) {
      var current = state.getCurrent(id);
      return DragTarget(
        builder: (BuildContext context, List candidateData,
            List rejectedData) {
          return Container(
            child: current,
            color: getColor(),
          );
        },
        onWillAccept: (data) {
          return current == null || data == current.id;
        },
        onAccept: (int data) {
          state.finishMove(data, id);
        },
      );
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

The last change we have to make is to simply the parent Widget. We have to provide the Board to the underlying Widgets and we have to provide the Widgets as children to the GridView.

        body: ChangeNotifierProvider(
          create: (context) => Board(),
          child: Consumer(builder: (context, state, child) {
            return GridView.count(
              physics: new NeverScrollableScrollPhysics(),
              padding: const EdgeInsets.all(10),
              crossAxisCount: 8,
              children: state.grid(),
            );
          }),
        ),

Enter fullscreen mode Exit fullscreen mode

That is it! We found it really simple compared to other languages and frameworks. If you are interested in all of the code, you can find it on Github. When you still have any questions or suggestions, feel free to leave a comment! Thanks for reading.

The post Flutter Drag and Drop Example appeared first on Barttje.

Top comments (0)