Recently, I have gradually updating common examples when using the 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.
This is what it shows at the end of the article:
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.
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.
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:
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:
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.
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.
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.
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.
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:
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.
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:
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:
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:
Top comments (0)