Dev-Owl / advanced_datatable

Advanced Datatable uses the Fultter PaginatedDataTable Widget and adds a few more functions to it

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to do CRUD / filter operations properly

d3vc44r opened this issue · comments

Good day,
I would like to use your component along with SQL and currently, as a Flutter beginner, I am not quite sure how to implement the following.

1.) A large database table is to be loaded into the component as a snapshot.
2.) The user should be able to filter the displayed data
3.) New data should be added by the user and sent to the backend.

So far I have extended my table state by a number x of table entries after each user click next page, which works fine. Paginated navigating of back and forth works fine.

Question:
If I filter certain values and add new values, the new value is added on some page. As an example I filter for salmon, add salomon2 and I don't see salomon2 after salomon in my filtered table. In case I click through my table pages I find salomon2 later. Even if I don't filter, my entry appears somewhere.

Can you tell me how to modify my data properly without destroying my table navigation or how I refresh my already loaded data properly?

Thank you for any tips

Cheers d3vc44r

The example does a lot of the points you have questions on. Adding data to the table should trigger a reload, of course this doesn't mean that you see your new data depending on the order.

The important part is the backend, the backend example is also included (it's written in Dart too).

Maybe you can share some code of your current implementation that might help me to give you some advice.

My widget is currently implemented in the following way:

import 'package:flutter/material.dart';
import 'package:advanced_datatable/datatable.dart';
import 'package:provider/provider.dart';
import '../../providers/tableFilter.dart';

import 'NutritionAdvancedTableSource.dart';

class NutritionAdvancedTableWidget extends StatefulWidget {
  @override
  _NutritionAdvancedTableWidgetState createState() =>
      _NutritionAdvancedTableWidgetState();
}

class _NutritionAdvancedTableWidgetState
    extends State<NutritionAdvancedTableWidget> {
  var rowsPerPage = 50;
  var sortIndex = 0;
  var sortAsc = true;

  NutritionAdvancedTableSource? source;


  @override
  void didChangeDependencies() {
    source = NutritionAdvancedTableSource(context, rowsPerPage);
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    var tableFilter = Provider.of<TableFilter>(context);
    source!.filterServerSide(tableFilter.searchTextFilter);

    return AdvancedPaginatedDataTable(
      addEmptyRows: false,
      columnSpacing: 30,
      source: this.source!,
      columns: [
        DataColumn(label: Text('nutrition')),
        DataColumn(label: Text('kcal')),
        DataColumn(label: Text('protein')),
        DataColumn(label: Text('fat')),
        DataColumn(label: Text('carbs'))
      ],
      rowsPerPage: rowsPerPage,
      availableRowsPerPage: [rowsPerPage],
      onRowsPerPageChanged: (newRowsPerPage) {
        if (newRowsPerPage != null) {
          setState(() {
            rowsPerPage = newRowsPerPage;
          });
        }
      }
    );
  }

  void setSort(int i, bool asc) => setState(() {
        sortIndex = i;
        sortAsc = asc;
      });
}

And my DataSource

import 'package:advanced_datatable/advancedDataTableSource.dart';
import 'package:collector_app/providers/nutritions.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../models/nutrition.dart';
import '../../models/RowData.dart';
import '../NutritionTableDialog.dart';

typedef SelectedCallBack = Function(String id, bool newSelectState);

class NutritionAdvancedTableSource extends AdvancedDataTableSource<RowData> {
  final BuildContext _context;
  final int rowsPerPage;
  var lastSearchTerm = '';

  NutritionAdvancedTableSource(this._context, this.rowsPerPage);

  DataRow? getRow(int index) {
    if (lastDetails == null) {
      return null;
    }

    Nutrition currentRowData = lastDetails!.rows[index].value;
    return DataRow(
        cells: [
          DataCell(Text(currentRowData.title.toString())),
          DataCell(Text(currentRowData.kcal.toString())),
          DataCell(Text(currentRowData.protein.toString())),
          DataCell(Text(currentRowData.fat.toString())),
          DataCell(Text(currentRowData.carbs.toString()))
        ],
        onSelectChanged: (selected) {
          print(currentRowData.title);
          Navigator.of(this._context)
              .restorablePush(_dialogBuilder, arguments: currentRowData.id);
        });
  }

  static Route<Object?> _dialogBuilder(
      BuildContext context, Object? arguments) {
    final nutritionId = arguments as String;

    return DialogRoute<void>(
      context: context,
      builder: (BuildContext context) => NutritionTableDialog(nutritionId),
    );
  }

  void filterServerSide(String? filterQuery) {
    if (filterQuery != null) {
      lastSearchTerm = filterQuery.toLowerCase().trim();
      setNextView();
    }
  }

  @override
  Future<RemoteDataSourceDetails<RowData>> getNextPage(
      NextPageRequest pageRequest) async {
    var nutritions = Provider.of<Nutritions>(_context, listen: false);

    List<RowData> tableRows = [];
    if (lastSearchTerm.isNotEmpty) {
      tableRows = (await nutritions.getNutritionPagePerFilter(
              lastSearchTerm, pageRequest.offset, this.rowsPerPage))
          .cast<RowData>();
    } else {
      tableRows = await nutritions.getNutritionPagePerOffset(
          pageRequest.offset, this.rowsPerPage);
    }
    var totalAmount = await nutritions.getTotalCountOfNutritions();

    return RemoteDataSourceDetails<RowData>(totalAmount, tableRows,
        filteredRows: lastSearchTerm.isNotEmpty ? tableRows.length : null);
  }

  @override
  int get selectedRowCount => 0;
}

Probably I can reduce my problems considerably, if I always make a DB query instead of successively building up such an internal cache .

In principle, I always send my uuid to a dialog and then get the actual object from the state.

In the case of getNutritionPagePerFilter, I get the filtered result from the DB, which of course explains why I don't see it....I don't have any experience with the performance of sqlite queries and mobile, so I'm inclined to query them as little as possible....

My state implementation:
this.offset is first time -1 and offset 0

Future<List<RowData>> getNutritionPagePerOffset(
      int offset, int batchSize) async {
    int i = 0;

    if (this.offset < offset) {
      List<Map<String, Object?>> tableData =
          await DbService.getNutritionTableDataPaginatedPerOffset(
              batchSize, offset);
      this.offset = offset;
      List<Nutrition> nutrition = _convertNutritionDataFromDb(tableData);

      _nutritions.addAll(nutrition);
      return nutrition.map((item) {
        return RowData(i++, item);
      }).toList();
    }
    return _nutritions.skip(offset).take(batchSize).map((item) {
      return RowData(i++, item);
    }).toList();
  }

Thanks for any tips @Dev-Owl
Hopefully, I´m not doing it totally wrong

Keeping database requests to the required amount is never a bad idea, in general doing numerous tiny queries or a few big queries can result in the same issue.
Looking at your implementation, without knowing the actual amount of data, the default idea should be to load data "stateless". The idea is that you always "only" have the current page in memory, that doesn't mean your idea of caching a page is wrong. It depends on the amount and complexity of the query.

Let's say you have a filter active on your table and a user edits data and saves it, you only have two options:

  1. Reload the current page from the backend
  2. Reset the table state, remove the filter and go to page 1

To just reload the current page in the table you can await the dialog result and if required set forceRemoteReload to true and call notifyListeners() in your datasource.
Going back to page 1 and reload can be done by calling setNextView()

There is no golden rule how to deal with the datasource change in sorted and paged setup, it really depends on your workflow. You could also show a snackbar and say:
"Your data source has changed, should I reload it with the current filter?" and maybe over a "Reset filter" button as well.

One last thing from my side, why do you have a class RowData, you could directly use your Nutrition object? That would also remove the need to pass just the ID to the dialog.
If you check out the example, you can see that I also just use my data model CompanyContact the reason is that we have to transfer at the end always to a DataRow.

Thanks for your answers...I will think about it, it´s super helpful :)

I guess, I have used this wrapper to avoid any serialization/deserialization issues if I pass my nutrition object via navigator to my EditDialog. My last try failed with jsonencode
"Converting object to an encodable object failed: Instance of 'Nutrition'"
StandardMessageEncoder does not support complex models.....

Maybe, I have to implement here my own MessageCodec.
Another idea, I can test is to generate a map by this comment https://stackoverflow.com/a/66706481/1097472
This would probably work without additional effort because map is supported

I guess this is either done or no further help is needed, feel free to open the issue again in case you need something