Flutter Drag and drop listitem between multiple lists

I want to develop like drag one listitem to another list which may be empty. if dragitem placed on another list's particular item then dragged item will be added to that index of list to which it dragged and same thing will be applicable on same list also. i tried but i am not able to make it like scroll listview while dragitem selected and there are only 3 visible items.

I want to develop like this: https://i.stack.imgur.com/bdn1I.gif

Any help appreciated.


Solution 1:

what you are trying to develop can be done using a Draggable and a DragTarget but you have to be aware that both the Draggable and DragTarget should have the same data type

I will briefly explain both widgets, then following this paragraph I will post the full code, and then I will explain each section separately.

Draggable is the widget used to enable the user to drag a widget from a position to another but it can only be dropped and processed on a DragTarget widget of the same type

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) =>
      MaterialApp(
        home: Drag(),
      );
}

class Drag extends StatefulWidget {
  @override
  _DragState createState() => _DragState();
}

class _DragState extends State<Drag> {
  List listA = ["A", "B", "C"];
  List listB = ["D", "E", "F"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.greenAccent[200],
      body: SafeArea(
        child: Column(
          children: [
//            list view separated will build a widget between 2 list items to act as a separator
            Expanded(
                child: ListView.separated(
                  itemBuilder: _buildListAItems,
                  separatorBuilder: _buildDragTargetsA,
                  itemCount: listA.length,
                )),
            Expanded(
                child: ListView.separated(
                  itemBuilder: _buildListBItems,
                  separatorBuilder: _buildDragTargetsB,
                  itemCount: listB.length,
                )),
          ],
        ),
      ),
    );
  }

//  builds the widgets for List B items
  Widget _buildListBItems(BuildContext context, int index) {
    return Draggable<String>(
//      the value of this draggable is set using data
      data: listB[index],
//      the widget to show under the users finger being dragged
      feedback: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listB[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
//      what to display in the child's position when being dragged
      childWhenDragging: Container(
        color: Colors.grey,
        width: 40,
        height: 40,
      ),
//      widget in idle state
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listB[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }

//  builds the widgets for List A items
  Widget _buildListAItems(BuildContext context, int index) {
    return Draggable<String>(
      data: listA[index],
      feedback: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listA[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
      childWhenDragging: Container(
        color: Colors.grey,
        width: 40,
        height: 40,
      ),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listA[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }

//  will return a widget used as an indicator for the drop position
  Widget _buildDropPreview(BuildContext context, String value) {
    return Card(
      color: Colors.lightBlue[200],
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Text(
          value,
          style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }

//  builds DragTargets used as separators between list items/widgets for list A
  Widget _buildDragTargetsA(BuildContext context, int index) {
    return DragTarget<String>(
//      builder responsible to build a widget based on whether there is an item being dropped or not
      builder: (context, candidates, rejects) {
        return candidates.length > 0 ? _buildDropPreview(context, candidates[0]):
            Container(
              width: 5,
              height: 5,
            );
      },
//      condition on to accept the item or not
      onWillAccept: (value)=>!listA.contains(value),
//      what to do when an item is accepted
      onAccept: (value) {
        setState(() {
          listA.insert(index + 1, value);
          listB.remove(value);
        });
      },
    );
  }

//  builds drag targets for list B
  Widget _buildDragTargetsB(BuildContext context, int index) {
    return DragTarget<String>(
      builder: (context, candidates, rejects) {
        return candidates.length > 0 ? _buildDropPreview(context, candidates[0]):
        Container(
          width: 5,
          height: 5,
        );
      },
      onWillAccept: (value)=>!listB.contains(value),
      onAccept: (value) {
        setState(() {
          listB.insert(index + 1, value);
          listA.remove(value);
        });
      },
    );
  }
}

the following part is responsible for building Draggable widgets for ListB, This Draggable Widget will display the value of the list based on the index and the list item value will be set as the data of the draggable so that a dragTarget can process the draggrable.

//  builds the widgets for List B items
  Widget _buildListBItems(BuildContext context, int index) {
    return Draggable<String>(
//      the value of this draggable is set using data
      data: listB[index],
//      the widget to show under the users finger being dragged
      feedback: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listB[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
//      what to display in the child's position when being dragged
      childWhenDragging: Container(
        color: Colors.grey,
        width: 40,
        height: 40,
      ),
//      widget in idle state
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            listB[index],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }

this part of the code will process the draggables coming from listB (can also be listA if you modify the code), it will accept/reject draggables based on their values, it will display a widget if the draggable can be accepted, and it will setState with the modified lists so that a new state is created and the ui is changed.

//  builds DragTargets used as separators between list items/widgets for list A
  Widget _buildDragTargetsA(BuildContext context, int index) {
    return DragTarget<String>(
//      builder responsible to build a widget based on whether there is an item being dropped or not
      builder: (context, candidates, rejects) {
        return candidates.length > 0 ? _buildDropPreview(context, candidates[0]):
            Container(
              width: 5,
              height: 5,
            );
      },
//      condition on to accept the item or not
      onWillAccept: (value)=>!listA.contains(value),
//      what to do when an item is accepted
      onAccept: (value) {
        setState(() {
          listA.insert(index + 1, value);
          listB.remove(value);
        });
      },
    );
  }

finally in this code, as I mentioned above it only accepts draggables from list A to list B and vice versa but it won't accept draggables from the same list to the same list. On top of that there are some cases that I did not consider but you can, these cases are:

  1. placing an item on the top of the list
  2. placing an item on the bottom of the list
  3. moving an item from a list to an empty list

example 1

example 2

hope my answer is helpful!