• Ready for a large number of data. Building cells on demand.
  • Focused on Web/Desktop Applications.
  • Bidirectional scroll bars.
  • Resizable.
  • Highly customized.
  • Pinned columns.
  • Multiple sort.
  • Infinite scroll.

Usage

Get started

  DaviModel<Person>? _model;

  @override
  void initState() {
    super.initState();

    _model = DaviModel<Person>(rows: [
      Person('Landon', 19),
      Person('Sari', 22),
      Person('Julian', 37),
      Person('Carey', 39),
      Person('Cadu', 43),
      Person('Delmar', 72)
    ], columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(name: 'Age', intValue: (row) => row.age)
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Davi<Person>(_model);
  }

Model

Column

Columns fit

All columns will fit in the available width.

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', grow: 2, stringValue: (row) => row.name),
      DaviColumn(name: 'Age', grow: 1, intValue: (row) => row.age)
    ]);
    Davi<Person>(_model, columnWidthBehavior: ColumnWidthBehavior.fit);

Stretchable column

The remaining width will be distributed to the columns according to the value of the grow attribute.

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', grow: 1, stringValue: (row) => row.name),
      DaviColumn(name: 'Age', intValue: (row) => row.age)
    ]);
  Davi<Person>(_model);

Column style

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(
          name: 'Age',
          intValue: (row) => row.age,
          headerTextStyle: TextStyle(color: Colors.blue[900]!),
          headerAlignment: Alignment.center,
          cellAlignment: Alignment.center,
          cellTextStyle: TextStyle(color: Colors.blue[700]!),
          cellBackground: (data) => Colors.blue[50])
    ]);

Pinned column

    _model = DaviModel(rows: persons, columns: [
      DaviColumn(
          pinStatus: PinStatus.left,
          width: 30,
          cellBuilder: (BuildContext context, DaviRow<Person> row) {
            return InkWell(
                child: const Icon(Icons.edit, size: 16),
                onTap: () => _onEdit(row.data));
          }),
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(name: 'Age', intValue: (row) => row.age)
    ]);

Row

Row color

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(name: 'Age', intValue: (row) => row.age)
    ]);
  @override
  Widget build(BuildContext context) {
    return Davi<Person>(_model, rowColor: _rowColor);
  }

  Color? _rowColor(DaviRow<Person> row) {
    if (row.data.age < 20) {
      return Colors.green[50]!;
    } else if (row.data.age > 30 && row.data.age < 50) {
      return Colors.orange[50]!;
    }
    return null;
  }

Row cursor

    DaviTheme(
        data: const DaviThemeData(
            row: RowThemeData(cursorOnTapGesturesOnly: false)),
        child: Davi<Person>(_model,
            rowCursor: (row) =>
                row.data.age < 20 ? SystemMouseCursors.forbidden : null));

Row callbacks

@override
Widget build(BuildContext context) {
  return Davi<Person>(_model,
      onRowTap: (person) => _onRowTap(context, person),
      onRowSecondaryTap: (person) => _onRowSecondaryTap(context, person),
      onRowDoubleTap: (person) => _onRowDoubleTap(context, person));
}

void _onRowTap(BuildContext context, Person person) {
  ...
}

void _onRowSecondaryTap(BuildContext context, Person person) {
  ...
}

void _onRowDoubleTap(BuildContext context, Person person) {
  ...
}

Row hover listener

  Davi<Person>(_model, onHover: _onHover);

  void _onHover(int? rowIndex) {
    ...
  }

Infinite scroll

  DaviModel<Value>? _model;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    List<Value> rows = List.generate(30, (index) => Value(index));
    _model = DaviModel<Value>(rows: rows, columns: [
      DaviColumn(name: 'Index', intValue: (row) => row.index),
      DaviColumn(name: 'Random 1', stringValue: (row) => row.random1),
      DaviColumn(name: 'Random 2', stringValue: (row) => row.random2)
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Davi<Value>(_model,
        lastRowWidget: const LoadingWidget(),
        onLastRowWidget: _onLastRowWidget);
  }

  void _onLastRowWidget(bool visible) {
    if (visible && !_loading) {
      setState(() {
        _loading = true;
      });
      Future.delayed(const Duration(seconds: 2), () {
        setState(() {
          _loading = false;
          List<Value> newValues =
              List.generate(15, (index) => Value(_model!.rowsLength + index));
          _model!.addRows(newValues);
        });
      });
    }
  }

Cell

Cell style

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(
          name: 'Age',
          intValue: (row) => row.age,
          cellStyleBuilder: (row) => row.data.age >= 30 && row.data.age < 40
              ? CellStyle(
                  background: Colors.blue[800],
                  alignment: Alignment.center,
                  textStyle: const TextStyle(color: Colors.white))
              : null)
    ]);

Custom cell widget

    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(
          name: 'Rate',
          width: 150,
          cellBuilder: (context, row) => StarsWidget(stars: row.data.stars))
    ]);

Cell edit

class Person {
  Person(this.name, this.value);

  final String name;
  final int value;

  bool _valid = true;

  bool get valid => _valid;

  String _editable = '';

  String get editable => _editable;

  set editable(String value) {
    _editable = value;
    _valid = _editable.length < 6;
  }
}

class MainWidgetState extends State<MainWidget> {
  DaviModel<Person>? _model;

  @override
  void initState() {
    super.initState();
    List<Person> rows = [
      Person('Landon', 1),
      Person('Sari', 0),
      Person('Julian', 2),
      Person('Carey', 4),
      Person('Cadu', 5),
      Person('Delmar', 2)
    ];
    _model = DaviModel<Person>(rows: rows, columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(name: 'Value', intValue: (row) => row.value),
      DaviColumn(
          name: 'Editable',
          cellBuilder: _buildField,
          cellBackground: (row) => row.data.valid ? null : Colors.red[800])
    ]);
  }

  Widget _buildField(BuildContext context, DaviRow<Person> row) {
    return TextFormField(
        initialValue: row.data.editable,
        style: TextStyle(color: row.data.valid ? Colors.black : Colors.white),
        onChanged: (value) => _onFieldChange(value, row.data));
  }

  void _onFieldChange(String value, Person person) {
    final wasValid = person.valid;
    person.editable = value;
    if (wasValid != person.valid) {
      setState(() {
        // rebuild
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Davi<Person>(_model);
  }
}

Sort

Multiple sort

    DaviModel<Person>(
        rows: rows,
        columns: [
          DaviColumn(name: 'Name', stringValue: (row) => row.name),
          DaviColumn(name: 'Age', intValue: (row) => row.age),
          DaviColumn(
              name: 'Weight', width: 120, doubleValue: (row) => row.weight)
        ],
        multiSortEnabled: true);

Sort callback

    _model = DaviModel<Person>(
        rows: rows,
        columns: [
          DaviColumn(name: 'Name', stringValue: (row) => row.name),
          DaviColumn(name: 'Age', intValue: (row) => row.age)
        ],
        onSort: _onSort);
  void _onSort(List<DaviColumn<Person>> sortedColumns) {
    ...
  }

Server-side sorting

Ignoring sorting functions from the model. Simulating the server-side sorting when loading data.

class Person {
  Person(this.name, this.age);

  final String name;
  final int age;
}

enum ColumnId { name, age }

class MainWidgetState extends State<MainWidget> {
  late DaviModel<Person> _model;
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _model = DaviModel<Person>(columns: [
      DaviColumn(
          id: ColumnId.name, name: 'Name', stringValue: (row) => row.name),
      DaviColumn(id: ColumnId.age, name: 'Age', intValue: (row) => row.age)
    ], onSort: _onSort, ignoreDataComparators: true);
    loadData();
  }

  void loadData([DaviSort? sort]) {
    Future<List<Person>>.delayed(const Duration(seconds: 1), () {
      List<Person> rows = [
        Person('Linda', 33),
        Person('Pamela', 22),
        Person('Steven', 21),
        Person('James', 37),
        Person('Amanda', 43),
        Person('Cadu', 35)
      ];
      if (sort != null) {
        final DaviSortDirection direction = sort.direction;
        rows.sort((a, b) {
          switch (sort.columnId) {
            case ColumnId.name:
              return direction == DaviSortDirection.ascending
                  ? a.name.compareTo(b.name)
                  : b.name.compareTo(a.name);
            case ColumnId.age:
              return direction == DaviSortDirection.ascending
                  ? a.age.compareTo(b.age)
                  : b.age.compareTo(a.age);
          }
          return 0;
        });
      }
      return rows;
    }).then((list) {
      if (mounted) {
        setState(() {
          _loading = false;
          _model.replaceRows(list);
        });
      }
    });
  }

  void _onSort(List<DaviColumn<Person>> sortedColumns) {
    setState(() {
      _loading = true;
      _model.removeRows();
    });
    loadData(sortedColumns.isNotEmpty ? sortedColumns.first.sort : null);
  }

  @override
  Widget build(BuildContext context) {
    return Davi(_model,
        tapToSortEnabled: !_loading,
        lastRowWidget:
            _loading ? const Center(child: Text('Loading...')) : null);
  }
}

Always sorted

Some sortable column will always be sorted.

    _model = DaviModel<Person>(
        rows: rows,
        columns: [
          DaviColumn(name: 'Name', stringValue: (row) => row.name),
          DaviColumn(name: 'Age', intValue: (row) => row.age),
          DaviColumn(
              name: 'Weight', width: 120, doubleValue: (row) => row.weight)
        ],
        alwaysSorted: true);

Theme

Dividers thickness and color

    DaviTheme(
        data: const DaviThemeData(
            columnDividerThickness: 4,
            columnDividerColor: Colors.blue,
            header: HeaderThemeData(columnDividerColor: Colors.purple),
            row: RowThemeData(dividerThickness: 4, dividerColor: Colors.green),
            scrollbar:
                TableScrollbarThemeData(columnDividerColor: Colors.orange)),
        child: Davi<Person>(_model));

    DaviTheme(
        data: DaviThemeData(
            header: HeaderThemeData(
                color: Colors.green[50],
                bottomBorderHeight: 4,
                bottomBorderColor: Colors.blue),
            headerCell: HeaderCellThemeData(
                height: 40,
                alignment: Alignment.center,
                textStyle: const TextStyle(
                    fontStyle: FontStyle.italic,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue),
                resizeAreaWidth: 10,
                resizeAreaHoverColor: Colors.blue.withOpacity(.5),
                sortIconColors: SortIconColors.all(Colors.green),
                expandableName: false)),
        child: Davi<Person>(_model));

Hidden header

    DaviTheme(
        data: const DaviThemeData(header: HeaderThemeData(visible: false)),
        child: Davi<Person>(_model));

Row

Theme Row color

    DaviTheme(
        data: DaviThemeData(
            row: RowThemeData(color: (rowIndex) => Colors.green[50])),
        child: Davi<Person>(_model));

Row zebra color

    DaviTheme(
        data:
            DaviThemeData(row: RowThemeData(color: RowThemeData.zebraColor())),
        child: Davi<Person>(_model));

Row hover background

    DaviTheme(
        data: DaviThemeData(
            row: RowThemeData(hoverBackground: (rowIndex) => Colors.blue[50])),
        child: Davi<Person>(_model));

Row hover foreground

    DaviTheme(
        data: DaviThemeData(
            row: RowThemeData(
                hoverForeground: (rowIndex) => Colors.blue.withOpacity(.2))),
        child: Davi<Person>(_model));

Row fill height

    DaviTheme(
        data: DaviThemeData(
            row: RowThemeData(
                fillHeight: true, color: RowThemeData.zebraColor())),
        child: Davi<Person>(_model));

Scrollbar

    DaviTheme(
        data: const DaviThemeData(
            scrollbar: TableScrollbarThemeData(
                thickness: 16,
                thumbColor: Colors.black,
                pinnedHorizontalColor: Colors.yellow,
                unpinnedHorizontalColor: Colors.green,
                verticalColor: Colors.blue,
                borderThickness: 8,
                pinnedHorizontalBorderColor: Colors.orange,
                unpinnedHorizontalBorderColor: Colors.purple,
                verticalBorderColor: Colors.pink)),
        child: Davi<Person>(_model));

Scrollbar always visible

    DaviTheme(
        data: const DaviThemeData(
            scrollbar: TableScrollbarThemeData(
                horizontalOnlyWhenNeeded: false,
                verticalOnlyWhenNeeded: false)),
        child: Davi<Person>(_model));

Cell

Null value color

    _model = DaviModel<Person>(rows: [
      Person('Landon', '+321 321-432-543'),
      Person('Sari', '+123 456-789-012'),
      Person('Julian', null),
      Person('Carey', '+111 222-333-444'),
      Person('Cadu', null),
      Person('Delmar', '+22 222-222-222')
    ], columns: [
      DaviColumn(name: 'Name', stringValue: (row) => row.name),
      DaviColumn(name: 'Mobile', width: 150, stringValue: (row) => row.mobile)
    ]);
    DaviTheme(
        data: DaviThemeData(
            cell: CellThemeData(
                nullValueColor: ((rowIndex, hovered) => Colors.grey[300]))),
        child: Davi<Person>(_model));

TODO

  • Collapsed rows
  • Header grouping
  • Row selection
  • Column reorder
  • Cell merge
  • Pinned column on right
  • Filter
  • And everything else, the sky is the limit

Support this project

Bitcoin

bc1qhqy84y45gya58gtfkvrvass38k4mcyqnav803h

Ethereum (ERC-20) or Binance Smart Chain (BEP-20)

0x9eB815FD4c88A53322304143A9Aa8733D3369985

Solana

7vp45LoQXtLYFXXKx8wQGnzYmhcnKo1TmfqUgMX45Ad8

Helium

13A2fDqoApT9VnoxFjHWcy8kPQgVFiVnzps32MRAdpTzvs3rq68

Libraries

davi