image

17 Dec 2025

9K

35K

Building a Drag-and-Drop Grid Widget in Flutter

Creating interactive user interfaces is a core aspect of modern application development. In Flutter, widgets are the fundamental building blocks, and leveraging their power allows developers to craft sophisticated UIs with relative ease. One common and highly interactive UI pattern is a grid of items that can be reordered via drag and drop. This article will guide you through the process of building such a widget in Flutter, combining `GridView`, `Draggable`, and `DragTarget` to achieve a seamless user experience.

Why Drag-and-Drop Grids?

Drag-and-drop functionality enhances user experience by providing an intuitive way to interact with lists or grids of data. For instance, users might want to reorder dashboard widgets, categorize items, or simply customize the layout of elements. Implementing this feature in a grid format allows for flexible spatial arrangement and dynamic reordering, making applications more engaging and user-friendly.

Core Concepts

Before diving into the implementation, let's understand the key Flutter widgets that make drag and drop possible:

  • GridView.builder: A widget that displays items in a 2D scrollable array. `GridView.builder` is efficient for a large number of items as it only builds the widgets that are currently visible.
  • Draggable: A widget that can be dragged from its position. It takes a generic type `T` which represents the data being dragged. `Draggable` provides `feedback` (what the user sees while dragging) and `childWhenDragging` (what is shown in the original position during the drag).
  • DragTarget: A widget that accepts data of a specific type `T` when it's dropped onto it. `DragTarget` has methods like `onWillAccept` (to determine if it will accept the dragged data), `onAccept` (called when data is dropped), and `builder` (to build the UI based on whether something is being dragged over it).

Project Setup

Let's start by creating a new Flutter project:


flutter create draggable_grid_example
cd draggable_grid_example

Then, replace the content of `lib/main.dart` with a basic structure:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Draggable Grid Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  // We'll manage our grid items here
  List _items = [];

  @override
  void initState() {
    super.initState();
    // Initialize with some dummy data
    for (int i = 0; i < 20; i++) {
      _items.add(GridItem(id: i, title: 'Item ${i + 1}'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Draggable Grid Demo'),
      ),
      body: Center(
        // Our DraggableGridView will go here
        child: DraggableGridView(
          items: _items,
          onReorder: (oldIndex, newIndex) {
            setState(() {
              final item = _items.removeAt(oldIndex);
              _items.insert(newIndex, item);
            });
          },
        ),
      ),
    );
  }
}

// Data model for our grid items
class GridItem {
  final int id;
  final String title;
  GridItem({required this.id, required this.title});
}

Data Model for Grid Items

First, define a simple data model for the items in our grid. This model will represent the data associated with each draggable tile.


// Already included in main.dart above
class GridItem {
  final int id;
  final String title;
  GridItem({required this.id, required this.title});
}

Implementing the DraggableGridView

Now, let's create the `DraggableGridView` widget. This widget will be a `StatelessWidget` and will take a list of `GridItem`s and a reorder callback.

We will implement the reordering logic within the `_MyHomePageState`, passing a callback to `DraggableGridView` for simplicity. This keeps the `DraggableGridView` focused on rendering and notifying of reorder events.

Create a new file `lib/draggable_grid_view.dart`:


import 'package:flutter/material.dart';
import 'main.dart'; // Assuming GridItem is defined in main.dart or a common file

class DraggableGridView extends StatefulWidget {
  final List items;
  final void Function(int oldIndex, int newIndex) onReorder;

  const DraggableGridView({
    Key? key,
    required this.items,
    required this.onReorder,
  }) : super(key: key);

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

class _DraggableGridViewState extends State {
  int? _draggingItemIndex;
  int? _targetItemIndex;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      itemCount: widget.items.length,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3, // Adjust as needed
        crossAxisSpacing: 8.0,
        mainAxisSpacing: 8.0,
      ),
      itemBuilder: (context, index) {
        return _buildGridItem(index);
      },
    );
  }

  Widget _buildGridItem(int index) {
    final item = widget.items[index];
    final bool isDragging = _draggingItemIndex == index;
    final bool isTarget = _targetItemIndex == index;

    return DragTarget(
      onWillAccept: (data) {
        if (data == null || data == index) {
          return false;
        }
        setState(() {
          _targetItemIndex = index;
        });
        return true;
      },
      onAccept: (data) {
        if (data != null) {
          widget.onReorder(data, _targetItemIndex!);
        }
        setState(() {
          _draggingItemIndex = null;
          _targetItemIndex = null;
        });
      },
      onLeave: (data) {
        setState(() {
          _targetItemIndex = null;
        });
      },
      builder: (BuildContext context, List candidateData, List rejectedData) {
        return Draggable(
          data: index, // The index of the item being dragged
          feedback: Material(
            elevation: 4.0,
            child: Container(
              padding: const EdgeInsets.all(8.0),
              color: Colors.blue.withOpacity(0.7),
              child: Text(
                item.title,
                style: const TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
          ),
          childWhenDragging: Container(
            color: Colors.grey.withOpacity(0.3), // Placeholder when item is dragging
            child: Center(child: Text(item.title)),
          ),
          onDragStarted: () {
            setState(() {
              _draggingItemIndex = index;
            });
          },
          onDraggableCanceled: (velocity, offset) {
            setState(() {
              _draggingItemIndex = null;
              _targetItemIndex = null;
            });
          },
          onDragEnd: (details) {
            setState(() {
              _draggingItemIndex = null;
              _targetItemIndex = null;
            });
          },
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 200),
            color: isDragging
                ? Colors.blue.withOpacity(0.3)
                : (isTarget ? Colors.green.withOpacity(0.5) : Colors.blue),
            child: Center(
              child: Text(
                item.title,
                style: const TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
          ),
        );
      },
    );
  }
}

And make sure to import `draggable_grid_view.dart` in `main.dart`:


import 'package:flutter/material.dart';
import 'package:draggable_grid_example/draggable_grid_view.dart'; // Add this line

// ... rest of your main.dart code

Explanation of the `DraggableGridView` Widget:

  • _draggingItemIndex and _targetItemIndex: These state variables track which item is currently being dragged and which grid position is currently being hovered over as a potential drop target. This allows us to apply visual feedback.
  • GridView.builder: This creates our scrollable grid. We specify a `gridDelegate` for a fixed number of columns.
  • _buildGridItem(int index): This method constructs each individual grid cell.
    • DragTarget: Each grid cell is wrapped in a `DragTarget`. The generic type `int` signifies that we expect to receive the index of the dragged item.
      • onWillAccept: Called when a draggable item enters the `DragTarget`'s bounds. If the `data` (index) is not null and not the current cell's index, we accept the drag and update `_targetItemIndex` to show a visual hint.
      • onAccept: Called when a draggable item is dropped onto this `DragTarget`. Here, we trigger the `onReorder` callback provided by the parent widget, passing the `oldIndex` (from `data`) and the `newIndex` (this cell's `index`). We then reset our tracking indices.
      • onLeave: Called when a draggable item leaves the `DragTarget`'s bounds. We clear the `_targetItemIndex`.
      • builder: This function builds the actual UI for the `DragTarget`. It receives information about what's being dragged over it.
    • Draggable: Inside the `DragTarget`'s `builder`, we place our actual `Draggable` item.
      • data: The data associated with this `Draggable`, which is the item's `index`. This is what the `DragTarget` will receive.
      • feedback: The widget that follows the user's finger while dragging. We create a `Material` widget to give it an elevated, distinct appearance.
      • childWhenDragging: The widget displayed in the `Draggable`'s original position while it is being dragged. We show a semi-transparent box.
      • onDragStarted, `onDraggableCanceled`, `onDragEnd` hooks: These methods are used to update `_draggingItemIndex` to reflect the current drag state, allowing for visual feedback on the item being dragged and resetting the state once the drag operation completes.
      • child: The widget that is normally displayed in the grid. We use an `AnimatedContainer` to smoothly transition colors based on `isDragging` or `isTarget` states.

Running the Application

You can now run your Flutter application:


flutter run

You should see a grid of numbered items. Try dragging an item and dropping it into a different position. The items should reorder smoothly!

Possible Enhancements

  • Placeholder Item Handling: When an item is dragged, its original position shows `childWhenDragging`. You could implement more sophisticated logic in `onWillAccept` to visually "shift" items to indicate where the dropped item would go, similar to the behavior in native reorderable lists.
  • Animations: For a smoother reordering experience, consider using `AnimatedList` or custom `ImplicitlyAnimatedWidget`s for transitions when items change position.
  • Custom Drag Feedback: The `feedback` widget can be customized extensively to match your app's design. You could display a more detailed card or a scaled-down version of the item.
  • Scroll on Drag: For large grids, you might want the grid to scroll automatically when a dragged item approaches the edge of the scrollable area. This requires more advanced gesture detection and scroll controller management.
  • Drag Constraints: Limit drag movement to certain axes or within specific boundaries using `Draggable`'s `axis` property or a custom `DragAnchorStrategy`.

Conclusion

Building a draggable grid in Flutter is a powerful way to enhance user interaction and customization in your applications. By understanding and combining the `Draggable`, `DragTarget`, and `GridView` widgets, you can create highly interactive and intuitive interfaces. The flexibility of Flutter's widget tree allows for granular control over the drag-and-drop experience, opening up a world of possibilities for dynamic UI designs.

Related Articles

Dec 18, 2025

Flutter &amp; Firebase Realtime Database: Data

Flutter &amp; Firebase Realtime Database: Data Synchronization In the realm of modern application development, providing users with up-to-date and consistent d

Dec 18, 2025

Building an Expandable FAQ Widget in Flutter

Building an Expandable FAQ Widget in Flutter Frequently Asked Questions (FAQ) sections are a common and essential component of many applications and websi

Dec 18, 2025

Flutter State Management with GetX Reactive

Flutter State Management with GetX Reactive Flutter's declarative UI paradigm simplifies application development, but managing application state effectively re