If DataTable is used on your application, you might want to select a row. Each row can be selected by clicking the line. It keeps the state until the same line is clicked again.
However, there are some cases where it needs to be unselected when another row is selected. This is the normal behavior when using a Desktop application. When you use an Explorer/File Browser, the shift key and control key is used to select multiple files/folders.
Let’s implement the same feature in Flutter with DataTable.
Set control/shift key press state
You need KeyboardListener
widget to detect a key press event. Check the following post if you don’t know how to use it.
Key detectction and the DataTable row selection need to be defined in a different function. Therefore, the following two variables need to be defined to know the key press state in the row select function.
bool isControlPressed = false;
bool isShiftPressed = false;
This is the code for onKeyEvent
in KeyBoardListener
. This callback set the key press state.
onKeyEvent: (value) {
debugPrint("Key: ${value.logicalKey.keyLabel}");
if (value.logicalKey == LogicalKeyboardKey.controlLeft ||
value.logicalKey == LogicalKeyboardKey.controlRight) {
setState(() {
isControlPressed = value is KeyDownEvent ? true : false;
text = "Control key ${isControlPressed ? "ON" : "OFF"}";
});
} else if (value.logicalKey == LogicalKeyboardKey.shiftLeft ||
value.logicalKey == LogicalKeyboardKey.shiftRight) {
setState(() {
isShiftPressed = value is KeyDownEvent
? true
: value is KeyUpEvent
? false
: isShiftPressed;
text = "Shift key ${isShiftPressed ? "ON" : "OFF"}";
});
}
},
I first used LogicalKeyboardKey.control
and LogicalKeyboardKey.shift
but it didn’t work as expected. Left and Right need to be used for the comparison.
The nested ternary operator is used in the code above for the shift key. It’s the same as the following code.
if (value is KeyDownEvent) {
isShiftPressed = true;
} else if (value is KeyUpEvent) {
isShiftPressed = false;
}
KeyEvent
is not only KeyDownEvent
and KeyUpEvent
but also KeyRepeatEvent
. Shift and Control keys don’t trigger KeyRepeatEvent
but only the right shift key triggers the event for some reason.
Selection handling
This is the main point of this post. The selection can be controlled with the key press state. The row class has selected property that is passed to DataRow.selected
.
class MyRowDataClass {
bool selected = false;
String text1;
String text2;
String text3;
MyRowDataClass({
required this.text1,
required this.text2,
required this.text3,
});
}
The main implementation needs to be written in DataRow.onSelectChanged
.
DataRow(
selected: row.selected,
cells: [
DataCell(Text(row.text1)),
DataCell(Text(row.text2)),
DataCell(Text(row.text3)),
],
onSelectChanged: (value) {
// implementation here
}
Select a row without any key
Firstly, select a row without any key. The selected row needs to be unselected when another row is selected.
if (!isControlPressed && !isShiftPressed) {
final selectedRows = data.where((element) => element.selected);
setState(() {
for (final row in selectedRows) {
row.selected = false;
}
row.selected = value ?? false;
});
}
The important thing here is to unselect rows that were selected before. Since multiple rows can be selected with the control or shift key, those must be unselected.
Select rows with control key
It’s simple logic while the control key is being pressed. This is the default behavior of DataTable.
else if (isControlPressed) {
setState(() {
row.selected = value ?? false;
});
}
Select rows with shift key
This is the most complex part of this feature.
onSelectChanged: (value) {
final currentIndex = data.indexOf(row);
if (!isControlPressed && !isShiftPressed) {
// without key
} else if (isControlPressed) {
// control
} else {
if (lastSelectedRowIndex == null) {
setState(() {
row.selected = value ?? false;
});
} else if (lastSelectedRowIndex! > currentIndex) {
final selectedIndexes =
List.generate(lastSelectedRowIndex! - currentIndex + 1, (index) => index + currentIndex);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
} else if (lastSelectedRowIndex! <= currentIndex) {
final selectedIndexes =
List.generate(currentIndex - lastSelectedRowIndex! + 1, (index) => index + lastSelectedRowIndex!);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
}
}
debugPrint(lastSelectedRowIndex.toString());
lastSelectedRowIndex = row.selected ? currentIndex : null;
},
Let’s check them one by one. If no row is selected, the behavior should be the same as the one for the control key.
if (lastSelectedRowIndex == null) {
setState(() {
row.selected = value ?? false;
});
}
While the shift key is being pressed, multiple rows need to be selected. Therefore, the range of the selection needs to be calculated. The number of rows to be selected can be calculated in the following formula.
- bigger number – smaller number + 1
The index is 0 based but the number starts with 1. That’s why + 1
is needed.
After getting the range, loop the list and set true or false depending on whether the index is in the range or not.
else if (lastSelectedRowIndex! > currentIndex) {
final selectedIndexes =
List.generate(lastSelectedRowIndex! - currentIndex + 1, (index) => index + currentIndex);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
}
There is almost the same logic.
else if (lastSelectedRowIndex! <= currentIndex) {
final selectedIndexes =
List.generate(currentIndex - lastSelectedRowIndex! + 1, (index) => index + lastSelectedRowIndex!);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
}
But it’s not good to write the same logic. So, let’s refactor it.
else {
final diff = lastSelectedRowIndex! - currentIndex;
final selectedIndexes = List.generate(
diff.abs() + 1,
(index) => index + min(lastSelectedRowIndex!, currentIndex),
);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
}
Update the last position
Then, the last thing is to update lastSelectedRowIndex
.
lastSelectedRowIndex = row.selected ? currentIndex : null;
When the shift key is being pressed, it’s not necessary to update it. Otherwise, update it.
Final code
The final code looks like this.
onSelectChanged: (value) {
final currentIndex = data.indexOf(row);
if (!isControlPressed && !isShiftPressed) {
final selectedRows = data.where((element) => element.selected);
setState(() {
for (final row in selectedRows) {
row.selected = false;
}
row.selected = value ?? false;
});
} else if (isControlPressed) {
setState(() {
row.selected = value ?? false;
});
} else {
if (lastSelectedRowIndex == null) {
setState(() {
row.selected = value ?? false;
});
} else {
final diff = lastSelectedRowIndex! - currentIndex;
final selectedIndexes = List.generate(
diff.abs() + 1,
(index) => index + min(lastSelectedRowIndex!, currentIndex),
);
setState(() {
for (int i = 0; i < data.length; i++) {
data[i].selected = selectedIndexes.contains(i) ? true : false;
}
});
return;
}
}
debugPrint(lastSelectedRowIndex.toString());
lastSelectedRowIndex = row.selected ? currentIndex : null;
},
If you want to check the completed code, you can find it in my GitHub repository.
Comments