Flutter Select rows on DataTable with the control/shift key

eye-catch Dart and Flutter

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.

Sponsored links

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.

Flutter How to delete a row on DataTable by Delete button
When showing data on DataTable, we might want to have a feature that deletes selected rows by pressing a Delete key. How is it possible? Use KeyboardListener.I will show you where to put the widget and how to use it in this post.

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.

Sponsored links

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

Copied title and URL