Drag and Drop Header for HorizontalDataTable
Tags: flutter, beginners
Published: 2022-07-21 12:30:39

Recently, I have gradually updating common examples when using the [horizontal_data_table](https://pub.dev/packages/horizontal_data_table) packages. One of the popular topic is enable to reorder the columns. This feature is really easy by working with the flutter Draggable and DropTarget Widget. The following is the steps I have been playing with the example SimpleTablePage and hope this can also be useful as an example using Draggable and DropTarget.

The whole class can check with the GitHub [repo](https://github.com/MayLau-CbL/flutter_horizontal_data_table/blob/master/example/lib/title_reorder_table.dart).

This is what it shows at the end of the article:

![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9ce9l8ev0r3fivofbip.gif)

Let's start!

First, extract the important information from each column to a private list. I have created a data class `UserColumnInfo` just to store the header name and width.

```dart
late List<UserColumnInfo> _colInfos;

@override
  void initState() {    
    super.initState();
    widget.user.initData(100);
    _colInfos = [
      const UserColumnInfo('Name', 100),
      const UserColumnInfo('Status', 100),
      const UserColumnInfo('Phone', 200),
      const UserColumnInfo('Register', 100),
      const UserColumnInfo('Termination', 200),
    ];
  }
```

Next, we start working on the header. If you trace to the `_getTitleWidget` function, you will see the header widgets are basically the same, the only different is their width.

```dart
List<Widget> _getTitleWidget() {
    return [
      _getTitleItemWidget('Name', 100),
      _getTitleItemWidget('Status', 100),
      _getTitleItemWidget('Phone', 200),
      _getTitleItemWidget('Register', 100),
      _getTitleItemWidget('Termination', 200),
    ];
  }
```

Then it is easy that we can apply the `_colInfos` to generate the list of widget easily, like this:

```dart
List<Widget> _getTitleWidget() {
    return _colInfos.map((e) => _getTitleItemWidget(e.name, e.width)).toList();
}
```

After all, everything is set. We can start our drag and drop implementation.

Since we are handling this with the Draggable and DropTarget, if you are not familiar with these two widget, it is recommend you to take a few minutes to watch this video from flutter.dev:

<iframe width="560" height="315" src="https://www.youtube.com/embed/QzA4c4QHZCY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

What I need to do is to prepare the Draggable widget header allows people drag and a DropTarget for other header to drop to. And this is it! I will explain more below. 

```dart
List<Widget> _getTitleWidget() {
    return _colInfos
        .map((e) => DragTarget(
              builder: (context, candidateData, rejectedData) {
                return Draggable<String>(
                  data: e.name,
                  feedback:
                      Material(child: _getTitleItemWidget(e.name, e.width)),
                  child: _getTitleItemWidget(e.name, e.width),
                );
              },
              onWillAccept: (value) {
                return value != e.name;
              },
              onAccept: (value) {
                int oldIndex =
                    _colInfos.indexWhere((element) => element.name == value);
                int newIndex =
                    _colInfos.indexWhere((element) => element.name == e.name);
                UserColumnInfo temp = _colInfos.removeAt(oldIndex);
                _colInfos.insert(newIndex, temp);
                setState(() {});
              },
            ))
        .toList();
  }
```

`builder` is to build the widget that the `DragTarget` is displaying. I build the Draggable inside to enable the child to be draggable to somewhere else. The `feedback` widget is wrapped the `Material` widget because the `feedback` widget is not inheriting the parent theme on default. If you want it to adopt the same UI as the existing header looks like. You need to let the child of `feedback` wrapped by the `Theme` widget. In this case, it is using the `Materail` theme and therefore I just simply use the `Material` widget to handle this issue. 

```dart
builder: (context, candidateData, rejectedData) {
  return Draggable<String>(
    data: e.name,
    feedback:
        Material(child: _getTitleItemWidget(e.name, e.width)),
    child: _getTitleItemWidget(e.name, e.width),
  );
}
```

`onWillAccept` is indicating which value is allowed to be accept. Since the interchange only allow the header is different to the existing position’s header, the value is set to not equal to the current header name.

```dart
onWillAccept: (value) {
  return value != e.name;
},
```

`onAccept` is handling the changes when the drop is accepted. I first find out the old and new index of the column. And then just simply remove and insert the column again.

```dart
onAccept: (value) {
  int oldIndex =
      _colInfos.indexWhere((element) => element.name == value);
  int newIndex =
      _colInfos.indexWhere((element) => element.name == e.name);
  UserColumnInfo temp = _colInfos.removeAt(oldIndex);
  _colInfos.insert(newIndex, temp);
  setState(() {});
}
```

While header part is finished, the body part needs to follow the change of columns. I use the similar approach for the body part. I first extract the table cell widgets. Since there are generally two types of cell, one is plain text and one is icon. I have these two functions:

```dart
Widget _generateGeneralColumnCell(
      BuildContext context, int rowIndex, int colIndex) {
    return Container(
      width: _colInfos[colIndex].width,
      height: 52,
      padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
      alignment: Alignment.centerLeft,
      child: Text(widget.user.userInfo[rowIndex].get(_colInfos[colIndex].name)),
    );
  }

  Widget _generateIconColumnCell(
      BuildContext context, int rowIndex, int colIndex) {
    return Container(
      width: 100,
      height: 52,
      padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
      alignment: Alignment.centerLeft,
      child: Row(
        children: <Widget>[
          Icon(
              widget.user.userInfo[rowIndex].status
                  ? Icons.notifications_off
                  : Icons.notifications_active,
              color: widget.user.userInfo[rowIndex].status
                  ? Colors.red
                  : Colors.green),
          Text(widget.user.userInfo[rowIndex].status ? 'Disabled' : 'Active')
        ],
      ),
    );
  }
```

You may notice these is a `get(_colInfos[colIndex].name)` for the getting the `UserInfo` field. Since the header is dynamic changing, the field cannot be hardcoded. I have added a function `get` in `UserInfo` class to get the field value by their header name.

```dart
dynamic get(String fieldName) {
  if (fieldName == 'Name') {
    return name;
  } else if (fieldName == 'Status') {
    return status;
  } else if (fieldName == 'Phone') {
    return phone;
  } else if (fieldName == 'Register') {
    return registerDate;
  } else if (fieldName == 'Termination') {
    return terminationDate;
  }
  throw Exception('Invalid field name');
}
```

The `HorizontalDataTable` left hand side and right hand side builder will also changed as following like the header:

```dart
Widget _generateFirstColumnRow(BuildContext context, int rowIndex) {
  if (_colInfos.first.name == 'Status') {
    return _generateIconColumnCell(context, rowIndex, 0);
  } else {
    return _generateGeneralColumnCell(context, rowIndex, 0);
  }
}

Widget _generateRightHandSideColumnRow(BuildContext context, int rowIndex) {
  return Row(
    children: _colInfos.sublist(1).map((e) {
      if (e.name == 'Status') {
        return _generateIconColumnCell(
            context, rowIndex, _colInfos.indexOf(e));
      } else {
        return _generateGeneralColumnCell(
            context, rowIndex, _colInfos.indexOf(e));
      }
    }).toList(),
  );
}
```

Finally, since the column will change to different order, the column width will also changed. The total width of the fixed side column and the bi-directional side need to be update as follow:

```dart
double get _sumOfRightColumnWidth {
  return _colInfos
      .sublist(1)
      .map((e) => e.width)
      .fold(0, (previousValue, element) => previousValue + element);
}

HorizontalDataTable(
  leftHandSideColumnWidth: _colInfos.first.width,
  rightHandSideColumnWidth: _sumOfRightColumnWidth,
	...
)
```

This is what it looks like now:

![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9ce9l8ev0r3fivofbip.gif)



**Support me if you like the content🍖**
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8D1EWN)




**Connect🍻**
[![GitHub - MayLau-CbL](https://img.shields.io/badge/GitHub-MayLau--CbL-informational?style=for-the-badge&logo=github)](https://github.com/MayLau-CbL)

[![Twitter - @MayLauDev](https://img.shields.io/badge/Twitter-%40MayLauDev-informational?style=for-the-badge&logo=twitter)](https://twitter.com/MayLauDev)

[![Hashnode - @MayLau](https://img.shields.io/badge/Hashnode-%40MayLau-informational?style=for-the-badge&logo=hashnode)](https://maylau.hashnode.dev/)