Gustavo Garcia
Gustavo Garcia

Reputation: 3223

Fixed column and row header for DataTable on Flutter Dart

I've build a table on Flutter Dart using DataTable. This table is very large, and I'm using both Vertical and Horizontal scrolling. When scrolling I lose reference to columns, I need to know what is the column.

As example. On the screenshot i don't know what the numbers 20.0 and 25.0 on the means, unless I scroll to the top.

I've added a GIF example of what i want to achieve. (Using LibreOffice). I need fixed column name (first row).

Example of the table, while scrolling around the middle of the table: Table without header for reference

Example of what i want to do: LibreOffice Calc Example

Code sample for my table:

    return SingleChildScrollView(
      scrollDirection: Axis.vertical,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: DataTable(
          columns: MyDataSet.getColumns(),
          rows: widget._data.map<DataRow>((row) => DataRow(
            onSelectChanged: (d) {
              setState(() {
                selectedRow = d ? row.hashCode : null;
              });
            },
            selected: row.hashCode == selectedRow,
            cells: MyDataSet.toDataCells(row)
          )).toList()
        )
      ),
    );

Missing code sample:

return columns.map<DataColumn>((name) => DataColumn(
      label: Text(name, style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),)
    )).toList();

Update (24/10/2019)

Current code works well if header name is the same size as cell content. Otherwise both sizes will be different.

Different sizes


Update (21/02/2020)

People created a package to do that. :D

https://pub.dev/packages/table_sticky_headers

Image from pub.dev! Pub.dev example image.

Upvotes: 25

Views: 51473

Answers (5)

Muhamad Raffi Irawan
Muhamad Raffi Irawan

Reputation: 51

This is what I did to make the header stay at the top using the table widget, hope it helps.

Table(
        border: TableBorder.all(),
        columnWidths: const {
          0: FlexColumnWidth(0.2),
          1: FlexColumnWidth(1),
          2: FlexColumnWidth(2),
          3: FlexColumnWidth(2),
          4: FlexColumnWidth(1),
        },
        defaultVerticalAlignment: TableCellVerticalAlignment.middle,
        children: [
          TableRow(
            children: ['No', 'Kode', 'Nama', 'Grup', 'Aksi'].map(
              (e) {
                return Padding(
                  padding: const EdgeInsets.all(10),
                  child: Text(
                    e,
                    textAlign: TextAlign.center,
                  ),
                );
              },
            ).toList(),
          ),
        ],
      ),
      Container(
        constraints: const BoxConstraints(maxHeight: 400),
        decoration: const BoxDecoration(border: Border(bottom: BorderSide())),
        child: SingleChildScrollView(
          child: Table(
            border: const TableBorder(
              left: BorderSide(),
              right: BorderSide(),
              horizontalInside: BorderSide(),
              verticalInside: BorderSide(),
            ),
            columnWidths: const {
              0: FlexColumnWidth(0.2),
              1: FlexColumnWidth(1),
              2: FlexColumnWidth(2),
              3: FlexColumnWidth(2),
              4: FlexColumnWidth(1),
            },
            defaultVerticalAlignment: TableCellVerticalAlignment.middle,
            children: controller.sectionCacat.asMap().entries.map((e) {
              return TableRow(
                children: [
                  ...[
                    (e.key + 1).toString(),
                    e.value.kode.toString(),
                    e.value.nama,
                    e.value.group,
                  ].map((e) {
                    return Padding(
                      padding: const EdgeInsets.all(10),
                      child: Text(
                        e,
                        textAlign: TextAlign.center,
                      ),
                    );
                  }),
                  Padding(
                    padding: const EdgeInsets.all(10),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        IconButton(
                          onPressed: () {},
                          icon: const Icon(Icons.edit),
                        ),
                        const SizedBox(width: 10),
                        IconButton(
                          onPressed: () {},
                          icon: const Icon(Icons.delete),
                        ),
                      ],
                    ),
                  ),
                ],
              );
            }).toList(),
          ),
        ),
      ),

Upvotes: 1

Kishan Donga
Kishan Donga

Reputation: 3193

Below Given Pablo Barrera answer is quite interesting, I have corrected and modified this answer with an enhanced feature, also it is easy to handle DataTable with fixed rows and columns. This DataTable you can customize as per your requirements.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Expanded(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: CustomDataTable(
              fixedCornerCell: '',
              borderColor: Colors.grey.shade300,
              rowsCells: _rowsCells,
              fixedColCells: _fixedColCells,
              fixedRowCells: _fixedRowCells,
            ),
          ),
        ),
      ),
    );
  }
}

final _rowsCells = [
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8]
];
final _fixedColCells = [
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
];
final _fixedRowCells = [
  "Math",
  "Informatics",
  "Geography",
  "Physics",
  "Biology"
];

class CustomDataTable<T> extends StatefulWidget {
  final T fixedCornerCell;
  final List<T> fixedColCells;
  final List<T> fixedRowCells;
  final List<List<T>> rowsCells;
  final double fixedColWidth;
  final double cellWidth;
  final double cellHeight;
  final double cellMargin;
  final double cellSpacing;
  final Color borderColor;

  const CustomDataTable({
    super.key,
    required this.fixedCornerCell,
    required this.fixedColCells,
    required this.fixedRowCells,
    required this.rowsCells,
    this.fixedColWidth = 60.0,
    this.cellHeight = 56.0,
    this.cellWidth = 120.0,
    this.cellMargin = 10.0,
    this.cellSpacing = 10.0,
    required this.borderColor,
  });

  @override
  State<StatefulWidget> createState() => CustomDataTableState();
}

class CustomDataTableState<T> extends State<CustomDataTable<T>> {
  final _columnController = ScrollController();
  final _rowController = ScrollController();
  final _subTableYController = ScrollController();
  final _subTableXController = ScrollController();

  Widget _buildChild(double width, T data) => SizedBox(
        width: width,
        child: Text(
          '$data',
          textAlign: TextAlign.center,
        ),
      );

  TableBorder _buildBorder({
    bool top = false,
    bool left = false,
    bool right = false,
    bool bottom = false,
    bool verticalInside = false,
  }) {
    return TableBorder(
      top: top ? BorderSide(color: widget.borderColor) : BorderSide.none,
      left: left ? BorderSide(color: widget.borderColor) : BorderSide.none,
      right: right ? BorderSide(color: widget.borderColor) : BorderSide.none,
      bottom: bottom ? BorderSide(color: widget.borderColor) : BorderSide.none,
      verticalInside: verticalInside
          ? BorderSide(color: widget.borderColor)
          : BorderSide.none,
    );
  }

  Widget _buildFixedCol() => DataTable(
      border: _buildBorder(right: true),
      horizontalMargin: widget.cellMargin,
      columnSpacing: widget.cellSpacing,
      headingRowHeight: widget.cellHeight,
      dataRowHeight: widget.cellHeight,
      columns: [
        DataColumn(
            label:
                _buildChild(widget.fixedColWidth, widget.fixedColCells.first))
      ],
      rows: widget.fixedColCells
          .map((c) =>
              DataRow(cells: [DataCell(_buildChild(widget.fixedColWidth, c))]))
          .toList());

  Widget _buildFixedRow() => DataTable(
        border: _buildBorder(verticalInside: true, bottom: true),
        horizontalMargin: widget.cellMargin,
        columnSpacing: widget.cellSpacing,
        headingRowHeight: widget.cellHeight,
        dataRowHeight: widget.cellHeight,
        columns: widget.fixedRowCells
            .map(
              (c) => DataColumn(
                label: _buildChild(widget.cellWidth, c),
              ),
            )
            .toList(),
        rows: const [],
      );

  Widget _buildSubTable() => Material(
      color: Colors.white,
      child: DataTable(
          border: _buildBorder(verticalInside: true),
          horizontalMargin: widget.cellMargin,
          columnSpacing: widget.cellSpacing,
          headingRowHeight: widget.cellHeight,
          dataRowHeight: widget.cellHeight,
          columns: widget.rowsCells.first
              .map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
              .toList(),
          rows: widget.rowsCells
              .map(
                (row) => DataRow(
                    cells: row
                        .map((c) => DataCell(_buildChild(widget.cellWidth, c)))
                        .toList()),
              )
              .toList()));

  Widget _buildCornerCell() => DataTable(
        border: _buildBorder(bottom: true, right: true),
        horizontalMargin: widget.cellMargin,
        columnSpacing: widget.cellSpacing,
        headingRowHeight: widget.cellHeight,
        dataRowHeight: widget.cellHeight,
        columns: [
          DataColumn(
            label: _buildChild(
              widget.fixedColWidth,
              widget.fixedCornerCell,
            ),
          )
        ],
        rows: const [],
      );

  @override
  void initState() {
    super.initState();
    _subTableXController.addListener(() {
      _rowController.jumpTo(_subTableXController.position.pixels);
    });
    _subTableYController.addListener(() {
      _columnController.jumpTo(_subTableYController.position.pixels);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(color: widget.borderColor),
      ),
      child: Column(
        children: [
          Row(
            children: [
              _buildCornerCell(),
              Flexible(
                child: SingleChildScrollView(
                  controller: _rowController,
                  scrollDirection: Axis.horizontal,
                  physics: const NeverScrollableScrollPhysics(),
                  child: _buildFixedRow(),
                ),
              ),
            ],
          ),
          Expanded(
            child: Row(
              children: [
                SingleChildScrollView(
                  controller: _columnController,
                  scrollDirection: Axis.vertical,
                  physics: const NeverScrollableScrollPhysics(),
                  child: _buildFixedCol(),
                ),
                Flexible(
                  child: SingleChildScrollView(
                    physics: const ClampingScrollPhysics(),
                    controller: _subTableXController,
                    scrollDirection: Axis.horizontal,
                    child: SingleChildScrollView(
                      physics: const ClampingScrollPhysics(),
                      controller: _subTableYController,
                      scrollDirection: Axis.vertical,
                      child: _buildSubTable(),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Output:

Upvotes: 4

Maxim Saplin
Maxim Saplin

Reputation: 4652

A few months back I had similar issue with limmited capabilities of stock DataTable and PaginatedDataTable2 widgets which didn't allow to fix the header. Eventually I took those widgets appart and created my own versions but with blackjack and fixed header row. Here's the plug-in on pub.dev: https://pub.dev/packages/data_table_2

The classes DataTable2 and PaginatedDataTable2 provide exactly the same APIs as the original versions.

NOTE: these one only implement sticky top rows, leftmost columns are not fixed/sticky

Upvotes: 11

Android_id
Android_id

Reputation: 1591

Try the flutter package horizontal_data_table
A Flutter Widget that create a horizontal table with fixed column on left hand side.

dependencies:
  horizontal_data_table: ^2.5.0

enter image description here

Upvotes: 2

Pablo Barrera
Pablo Barrera

Reputation: 10963

I could come up with a workaround using scroll controllers, looks like this: Video

Basically it's an horizontal scroll for the first row, a vertical scroll for the first column and a mixed horizontal and vertical scroll for the subtable. Then when you move the subtable, its controllers move the column and the row.

Here is a custom widget with an example of how to use it:

final _rowsCells = [
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8],
  [7, 8, 10, 8, 7],
  [10, 10, 9, 6, 6],
  [5, 4, 5, 7, 5],
  [9, 4, 1, 7, 8]
];
final _fixedColCells = [
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
  "Pablo",
  "Gustavo",
  "John",
  "Jack",
];
final _fixedRowCells = [
  "Math",
  "Informatics",
  "Geography",
  "Physics",
  "Biology"
];

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: CustomDataTable(
      rowsCells: _rowsCells,
      fixedColCells: _fixedColCells,
      fixedRowCells: _fixedRowCells,
      cellBuilder: (data) {
        return Text('$data', style: TextStyle(color: Colors.red));
      },
    ),
  );
}

class CustomDataTable<T> extends StatefulWidget {
  final T fixedCornerCell;
  final List<T> fixedColCells;
  final List<T> fixedRowCells;
  final List<List<T>> rowsCells;
  final Widget Function(T data) cellBuilder;
  final double fixedColWidth;
  final double cellWidth;
  final double cellHeight;
  final double cellMargin;
  final double cellSpacing;

  CustomDataTable({
    this.fixedCornerCell,
    this.fixedColCells,
    this.fixedRowCells,
    @required this.rowsCells,
    this.cellBuilder,
    this.fixedColWidth = 60.0,
    this.cellHeight = 56.0,
    this.cellWidth = 120.0,
    this.cellMargin = 10.0,
    this.cellSpacing = 10.0,
  });

  @override
  State<StatefulWidget> createState() => CustomDataTableState();
}

class CustomDataTableState<T> extends State<CustomDataTable<T>> {
  final _columnController = ScrollController();
  final _rowController = ScrollController();
  final _subTableYController = ScrollController();
  final _subTableXController = ScrollController();

  Widget _buildChild(double width, T data) => SizedBox(
      width: width, child: widget.cellBuilder?.call(data) ?? Text('$data'));

  Widget _buildFixedCol() => widget.fixedColCells == null
      ? SizedBox.shrink()
      : Material(
          color: Colors.lightBlueAccent,
          child: DataTable(
              horizontalMargin: widget.cellMargin,
              columnSpacing: widget.cellSpacing,
              headingRowHeight: widget.cellHeight,
              dataRowHeight: widget.cellHeight,
              columns: [
                DataColumn(
                    label: _buildChild(
                        widget.fixedColWidth, widget.fixedColCells.first))
              ],
              rows: widget.fixedColCells
                  .sublist(widget.fixedRowCells == null ? 1 : 0)
                  .map((c) => DataRow(
                      cells: [DataCell(_buildChild(widget.fixedColWidth, c))]))
                  .toList()),
        );

  Widget _buildFixedRow() => widget.fixedRowCells == null
      ? SizedBox.shrink()
      : Material(
          color: Colors.greenAccent,
          child: DataTable(
              horizontalMargin: widget.cellMargin,
              columnSpacing: widget.cellSpacing,
              headingRowHeight: widget.cellHeight,
              dataRowHeight: widget.cellHeight,
              columns: widget.fixedRowCells
                  .map((c) =>
                      DataColumn(label: _buildChild(widget.cellWidth, c)))
                  .toList(),
              rows: []),
        );

  Widget _buildSubTable() => Material(
      color: Colors.lightGreenAccent,
      child: DataTable(
          horizontalMargin: widget.cellMargin,
          columnSpacing: widget.cellSpacing,
          headingRowHeight: widget.cellHeight,
          dataRowHeight: widget.cellHeight,
          columns: widget.rowsCells.first
              .map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
              .toList(),
          rows: widget.rowsCells
              .sublist(widget.fixedRowCells == null ? 1 : 0)
              .map((row) => DataRow(
                  cells: row
                      .map((c) => DataCell(_buildChild(widget.cellWidth, c)))
                      .toList()))
              .toList()));

  Widget _buildCornerCell() =>
      widget.fixedColCells == null || widget.fixedRowCells == null
          ? SizedBox.shrink()
          : Material(
              color: Colors.amberAccent,
              child: DataTable(
                  horizontalMargin: widget.cellMargin,
                  columnSpacing: widget.cellSpacing,
                  headingRowHeight: widget.cellHeight,
                  dataRowHeight: widget.cellHeight,
                  columns: [
                    DataColumn(
                        label: _buildChild(
                            widget.fixedColWidth, widget.fixedCornerCell))
                  ],
                  rows: []),
            );

  @override
  void initState() {
    super.initState();
    _subTableXController.addListener(() {
      _rowController.jumpTo(_subTableXController.position.pixels);
    });
    _subTableYController.addListener(() {
      _columnController.jumpTo(_subTableYController.position.pixels);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Row(
          children: <Widget>[
            SingleChildScrollView(
              controller: _columnController,
              scrollDirection: Axis.vertical,
              physics: NeverScrollableScrollPhysics(),
              child: _buildFixedCol(),
            ),
            Flexible(
              child: SingleChildScrollView(
                controller: _subTableXController,
                scrollDirection: Axis.horizontal,
                child: SingleChildScrollView(
                  controller: _subTableYController,
                  scrollDirection: Axis.vertical,
                  child: _buildSubTable(),
                ),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            _buildCornerCell(),
            Flexible(
              child: SingleChildScrollView(
                controller: _rowController,
                scrollDirection: Axis.horizontal,
                physics: NeverScrollableScrollPhysics(),
                child: _buildFixedRow(),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

Since the first column, the first row and the subtable are independent, I had to create a DataTable for each one. And since DataTable has headers that can't be removed, the headers of the first column and the subtable are hidden by the first row.

Also, I had to make the first column and first row not manually scrollable because if you scroll them the subtable won't scroll.

This might not be the best solution, but at the moment doesn't seem to be another way to do it. You could try to improve this approach, maybe using Table or other widgets instead of DataTable at least you could avoid hiding the headers of the subtable and first column.

Upvotes: 21

Related Questions