Paginated View of data in Firebase into sfdatagrid

Hi, I wanna create a paginated store list in which it will show the row data according to rowperpage. The storelist will connect will the firebase directly, which means at page 1, it will retrieve 3 stores data from firebase and show in the page. Everytime press the next page, it will retrieve another 3 stores data from firebase. When we reverse back from page 4 to page 2, it will show the previous content in page 2. 


Beside, when we add/edit/delete the store data in firebase, the latest store list will be updated too.


Currently the first problem in my code is I able to fetch data into paginated view for page 1,2,3 and more. But when I want to reverse back from page 3 to page1, it will show nothing... 


Second problem is everytime add/edit/delete, the data in firebase need to update synchronize also, so i still not sure how to do it....


Attachment: Pagination_55798b50.rar

2 Replies

WP WONG PEI SAN A20EC0170 October 26, 2023 07:36 AM UTC

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:musix_v2/firestore/db_service.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import '../../firestore/models/stores_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

RxString view = 'viewStores'.obs;
String storeID = '';

//=========================
List<StoresModel> paginatedDataSource = [];
List<StoresModel> storeList = [];
final int rowsPerPage = 4;
double pageCount = 0;
double storesLength = 0.0;
//==============================

class StoresTable extends StatefulWidget {
const StoresTable({super.key});

@override
State<StoresTable> createState() => _StoresTableState();
}

class _StoresTableState extends State<StoresTable> {
bool isSwitched = false;

RxString text = 'Add Store'.obs;
bool isMainStore = false;
bool? isMain;
bool? textResult;
bool searchNotFound = false;

List<StoresModel> originalStoreList = [];
List<StoresModel> filteredStoreList = [];

TextEditingController controller = TextEditingController();
TextEditingController _controllerSName = TextEditingController();
TextEditingController _controllerSAddress = TextEditingController();

final _formKey = GlobalKey<FormBuilderState>();
final _sAddressFieldKey = GlobalKey<FormBuilderFieldState>();
final _sNameFieldKey = GlobalKey<FormBuilderFieldState>();

final DataGridController _dataGridController = DataGridController();

void updateButtonName() {
if (_dataGridController.selectedRows.isEmpty) {
text('Add Store');
} else {
text('Subscribe');
}
}

//==================
late StoreDataSource _storeDataSource;
bool showLoadingIndicator = true;
bool dataLoaded = false; // Flag to track if data has been loaded
//==================

Future<void> getStoresLength() async {
storesLength = (await DBService.getStoresLength()) as double;
setState(() {
dataLoaded = true; // Set the flag when data is loaded
});
}

Future<void> populateData() async {
storeList = await DBService.getStoresData();
_storeDataSource = StoreDataSource();
}

@override
void initState() {
super.initState();
populateData();
getStoresLength();
}

@override
Widget build(BuildContext context) {
if (dataLoaded == false) {
// Data is not yet loaded, show a loading indicator or a placeholder
return CircularProgressIndicator(); // or any other loading indicator.
}
return Obx(() {
if (view.value == 'viewStores') {
return LayoutBuilder(builder: (context, constraints) {
if (filteredStoreList.isNotEmpty ||
(filteredStoreList.isEmpty && searchNotFound == true))
storeList = filteredStoreList;
else if (filteredStoreList.isEmpty && searchNotFound == false)
storeList = originalStoreList;

pageCount = (storesLength / rowsPerPage).ceilToDouble();

return Container(
margin: EdgeInsets.only(
left: 24,
right: 24,
),
child: Column(children: [
SizedBox(
height: 40,
),
Container(
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Flexible(
flex: 3,
child: TextButton(
onPressed: () {
setState(() {
if (text.value == 'Add Store') {
view('addStores');
}
// else if (text.value == 'Subscribe') {
// view = 'subscribe';
// }
});
},
child: Obx(
() => Text(
text.value,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
style: TextButton.styleFrom(
backgroundColor: Color(0xFFF35539),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
),
Flexible(
flex: 3,
child: Container(
margin: EdgeInsets.only(left: 15, right: 15),
child: TextButton(
onPressed: () {},
child: Text(
'Reset Play Q',
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
style: TextButton.styleFrom(
backgroundColor: Color(0xFFF9E5E2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
),
),
Flexible(
flex: 2,
child: Container(
margin: EdgeInsets.only(right: 15),
child: SizedBox(
width: 178,
height: 28,
child: TextField(
controller: controller,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(bottom: 15.0),
filled: true,
fillColor: Colors.white,
prefixIcon: Icon(Icons.search),
hintText: "Search",
),
onSubmitted: (value) {
setState(() {
// Filter the store items based on the search input
setState(() {
filteredStoreList =
originalStoreList.where((element) {
return element.name?.contains(value) == true;
}).toList();

if (filteredStoreList.isEmpty) {
searchNotFound = true;
} else if (filteredStoreList.isNotEmpty) {
searchNotFound = false;
}
});
});
},
),
),
),
),
]),
),
Divider(
height: 10,
color: Color(0xFF9DA19D),
thickness: 1,
),
SizedBox(
width: constraints.maxWidth,
height: 425,
child: buildStack(constraints),
),
Container(
width: constraints.maxWidth,
child: SfDataPager(
pageCount: pageCount,
direction: Axis.horizontal,
delegate: _storeDataSource,
onPageNavigationStart: (int pageIndex) {
setState(() {
showLoadingIndicator = true;
});
},
onPageNavigationEnd: (int pageIndex) {
setState(() {
showLoadingIndicator = false;
});
},
),
),
]),
);
});
}
return Container();
});
}

Widget buildDataGrid(BoxConstraints constraint) {
return SfDataGrid(
source: _storeDataSource,
allowSorting: true,
showCheckboxColumn: true,
selectionMode: SelectionMode.multiple,
controller: _dataGridController,
showVerticalScrollbar: false,
onSelectionChanged: (a, b) {
updateButtonName();
},
columns: [
GridColumn(
columnName: 'storesName',
columnWidthMode: ColumnWidthMode.lastColumnFill,
label: Container(
alignment: Alignment.center,
child: Text(
'Store Name',
softWrap: false,
),
),
),
GridColumn(
columnName: 'storesPlayQ',
label: Container(
alignment: Alignment.center,
child: Text(
'Has Play Q?',
softWrap: false,
),
),
),
GridColumn(
columnName: 'plan',
label: Container(
alignment: Alignment.center,
child: Text(
'Plan',
softWrap: false,
),
),
),
GridColumn(
columnName: 'currency',
label: Container(
alignment: Alignment.center,
child: Text(
'Currency',
softWrap: false,
),
),
),
GridColumn(
columnName: 'storesSubsStartEnd',
minimumWidth: 190,
allowSorting: false,
label: Container(
alignment: Alignment.center,
child: Text(
'Billing Period',
softWrap: false,
),
),
),
GridColumn(
columnName: 'storesSubsEnd',
label: Container(
alignment: Alignment.center,
child: Text(
'Subscription End',
softWrap: false,
),
),
),
GridColumn(
columnName: 'storesStatus',
label: Container(
alignment: Alignment.center,
child: Text(
'Status',
softWrap: false,
),
),
),
GridColumn(
columnName: 'deleteStores',
allowSorting: false,
label: Container(
alignment: Alignment.center,
),
),
],
);
}

Widget buildStack(BoxConstraints constraints) {
print('1111111111');

List<Widget> _getChildren() {
final List<Widget> stackChildren = [];

stackChildren.add(buildDataGrid(constraints));

if (showLoadingIndicator) {
stackChildren.add(Container(
color: Colors.black12,
width: constraints.maxWidth,
height: 425,
child: Align(
alignment: Alignment.center,
child: CircularProgressIndicator(
strokeWidth: 3,
),
),
));
}

print('3333333333');
return stackChildren;
}

print('222222222222');
return Stack(
children: _getChildren(),
);
}
}

class StoreDataSource extends DataGridSource {
String checkStatus(DateTime? date) {
if (date == null) {
return 'null';
} else {
if (DateTime.now().isBefore(date)) {
return 'Active';
} else {
return 'Expired';
}
}
}

String checkStoresStartEndBill(StoresModel dataGridRow) {
if (dataGridRow.billingStartDt == null) {
return 'null';
} else {
return '${dataGridRow.billingStartDt?.toDate().day.toString()}/${dataGridRow.billingStartDt?.toDate().month.toString()}/${dataGridRow.billingStartDt?.toDate().year.toString()} - '
'${dataGridRow.billingEndDt?.toDate().day.toString()}/${dataGridRow.billingEndDt?.toDate().month.toString()}/${dataGridRow.billingEndDt?.toDate().year.toString()}';
}
}

String checkSubsEndDt(StoresModel dataGridRow) {
if (dataGridRow.subsEndDt == null) {
return 'null';
} else {
return '${dataGridRow.subsEndDt?.toDate().day.toString()}/${dataGridRow.subsEndDt?.toDate().month.toString()}/${dataGridRow.subsEndDt?.toDate().year.toString()}';
}
}

StoreDataSource() {
paginatedDataSource = storeList.getRange(0, 5).toList();
buildDataGridRows();
}

List<DataGridRow> _storesData = [];

@override
List<DataGridRow> get rows => _storesData;

@override
Future<bool> handlePageChange(int oldPageIndex, int newPageIndex) async {
await Future.delayed(const Duration(seconds: 3));
int startIndex = newPageIndex * rowsPerPage;
int endIndex = startIndex + rowsPerPage;
if (startIndex < pageCount) {
if (endIndex > storesLength) {
endIndex = storesLength as int;
}
// paginatedDataSource =
// storeList.getRange(startIndex, endIndex).toList(growable: false);
buildDataGridRows();
} else {
paginatedDataSource = [];
}
notifyListeners();
return true;
//
// print('aaaaaaaaaa');
//
// try {
// paginatedDataSource =
// await DBService.getStoresForPage(newPageIndex, rowsPerPage);
// buildDataGridRows();
// } catch (error) {
// print('Error: $error');
// }
//
// print('bbbbbbb');
// notifyListeners();
// return true;
}

void buildDataGridRows() {
_storesData = paginatedDataSource.map<DataGridRow>((dataGridRow) {
return DataGridRow(cells: [
DataGridCell<String>(columnName: 'name', value: dataGridRow.id),
DataGridCell<String>(columnName: 'hasPlayedQ', value: dataGridRow.id),
DataGridCell<String>(
columnName: 'plan', value: dataGridRow.plan.toString()),
DataGridCell<String>(
columnName: 'currency', value: dataGridRow.currency.toString()),
DataGridCell<String>(
columnName: 'storesStartEndBil',
value: checkStoresStartEndBill(dataGridRow)),
DataGridCell<String>(
columnName: 'subsEndDt', value: checkSubsEndDt(dataGridRow)),
DataGridCell<String>(
columnName: 'storesStatus',
value: checkStatus(dataGridRow.subsEndDt?.toDate())),
DataGridCell<String>(
columnName: 'deleteStores',
value: dataGridRow.id,
),
]);
}).toList();
}

@override
DataGridRowAdapter buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells: row.getCells().map<Widget>((e) {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(12),
child: FutureBuilder<StoresModel?>(
future: DBService.getStoresModel(e.value),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
if (e.columnName == 'name') {
// Check if snapshot.data is not null
String storeName =
snapshot.data!.name ?? "Store Name Not Found";
return Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 2,
child: Text(
storeName,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 5),
IconButton(
splashRadius: 0.0001,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 22, maxWidth: 22),
icon: const Icon(
Icons.edit,
size: 18,
),
onPressed: () {
storeID = e.value;
view('editStores');
},
),
if (snapshot.data!.isMain == true)
Image(
image: AssetImage('assets/Flag.png'),
fit: BoxFit.cover,
),
SizedBox(width: 5),
],
);
} else if (e.columnName == 'hasPlayedQ') {
return Container(
child: Padding(
padding: const EdgeInsets.only(left: 15),
child: Row(
children: [
snapshot.data!.hasPlayedQ!
? Icon(
Icons.check,
size: 15,
)
: Icon(
Icons.close,
size: 15,
),
SizedBox(width: 5),
IconButton(
splashRadius: 0.0001,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 22, maxWidth: 22),
icon: const Icon(
Icons.edit,
size: 18,
),
onPressed: () {},
),
],
),
),
);
} else if (e.columnName == 'deleteStores') {
return InkWell(
child: Icon(
Icons.delete,
size: 20,
),
onTap: () async {
storeID = e.value;
Get.defaultDialog(
title: '${await DBService.getStoreNameById(storeID)}',
middleText: 'Do you want to delete this store?',
actions: [
ElevatedButton(
onPressed: () async {
await DBService.removeStore(e.value);
view('viewStores');
Get.back();
},
child: Text('Yes'),
),
ElevatedButton(
onPressed: () {
Get.back();
},
child: Text('No'),
),
],
);
},
);
}
}
return Text(e.value);
},
),
);
}).toList(),
);
}
}


/////////////////////////database/////////////////

static DocumentSnapshot?
lastDocument; // Store the reference to the last document

static Future<List<StoresModel>> getStoresForPage(int x, int y) async {
final List<StoresModel> storesList = [];

try {
final userController = Get.find<UserController>();
final companyId = userController.companyId;

QuerySnapshot query = await FirebaseFirestore.instance
.collection(kCompanyCollection)
.doc(companyId)
.collection(kStoresCollection)
.startAt([0]).endAt([2]).get();

if (query.docs.isNotEmpty) {
for (QueryDocumentSnapshot document in query.docs) {
final data = document.data() as Map<String, dynamic>;
final storesListModel = StoresModel.fromMap(document.id, data);
storesList.add(storesListModel);
}
}
} catch (e) {
print("Error retrieving data: $e");
}

return storesList;
}

static Future<List<StoresModel>> getStoresData() async {
final List<StoresModel> storesList = [];

try {
final userController = Get.find<UserController>();
final companyId = userController.companyId;

QuerySnapshot query = await FirebaseFirestore.instance
.collection(kCompanyCollection)
.doc(companyId)
.collection(kStoresCollection)
.limit(10)
.get();

if (query.docs.isNotEmpty) {
for (QueryDocumentSnapshot document in query.docs) {
final data = document.data() as Map<String, dynamic>;
final storesListModel = StoresModel.fromMap(document.id, data);
storesList.add(storesListModel);
}
}
} catch (e) {
print("Error retrieving data: $e");
}

return storesList;
}


///////////////////////storemodel///////////////////////

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:musix_v2/firestore/models/users_model.dart';

const kStoreId = 'id';
const kStoreName = 'name';
const kStoreAddress = 'address';
const kIsMain = 'isMain';
const kTrialEndDt = 'trialEndDt';
const kCurrency = 'currency';
const kBillingStartDt = 'billingStartDt';
const kBillingEndDt = 'billingEndDt';
const kSubsEndDt = 'subsEndDt';
const kPlan = 'plan';
const kRenewal = 'renewal';
const kCreatedAt = 'createdAt';
const kCreatedBy = 'createdBy';
const kUpdatedAt = 'updatedAt';
const kUpdatedBy = 'updatedBy';
const kHasPlayQ = 'hasPlayedQ';

class StoresModel {
String? id;
String? name;
String? address;
bool? isMain;
Timestamp? trialEndDt;
String? currency;
Timestamp? billingStartDt;
Timestamp? billingEndDt;
Timestamp? subsEndDt;
String? plan;
num? renewal;
Timestamp? createdAt;
String? createdBy;
Timestamp? updatedAt;
String? updatedBy;
bool? hasPlayedQ;

List<UserModel>? users;

StoresModel({
this.id,
this.name,
this.address,
this.isMain,
this.trialEndDt,
this.currency,
this.billingStartDt,
this.billingEndDt,
this.subsEndDt,
this.plan,
this.renewal,
this.createdAt,
this.createdBy,
this.updatedAt,
this.updatedBy,
this.hasPlayedQ,
this.users,
});

Map<String, dynamic> setData({
String? newStoresName,
String? newStoresAddress,
bool? newStoresIsMain,
Timestamp? newStoresCreatedAt,
String? newStoresCreatedBy,
Timestamp? newTrialEndDt,
}) {
return {
'name': newStoresName ?? name,
'address': newStoresAddress ?? address,
'isMain': newStoresIsMain ?? isMain,
'createdAt': newStoresCreatedAt ?? createdAt,
'createdBy': newStoresCreatedBy ?? createdBy,
'hasPlayedQ': hasPlayedQ ?? false,
'trialEndDt': trialEndDt,
'currency': currency,
'billingStartDt': billingStartDt,
'billingEndDt': billingEndDt,
'subsEndDt': subsEndDt,
'plan': plan,
'renewal': renewal,
'updatedAt': updatedAt,
'updatedBy': updatedBy,
};
}

StoresModel.fromMap(String this.id, Map<String, dynamic> data)
: name = data[kStoreName] ?? '',
address = data[kStoreAddress] ?? '',
isMain = data[kIsMain] ?? false,
trialEndDt = data[kTrialEndDt],
currency = data[kCurrency],
billingStartDt = data[kBillingStartDt],
billingEndDt = data[kBillingEndDt],
subsEndDt = data[kSubsEndDt],
plan = data[kPlan],
renewal = data[kRenewal],
createdAt = data[kCreatedAt] as Timestamp,
createdBy = data[kCreatedBy] ?? '',
updatedAt = data[kUpdatedAt],
updatedBy = data[kUpdatedBy] ?? '',
hasPlayedQ = data[kHasPlayQ] ?? false;
}



TP Tamilarasan Paranthaman Syncfusion Team October 31, 2023 03:55 PM UTC

Hi WONG PEI SAN,

Regarding: Currently the first problem in my code is I able to fetch data into paginated view for page 1,2,3 and more. But when I want to reverse back from page 3 to page1, it will show nothing.


To meet your requirements of loading data from Firestore, you can retrieve the necessary data from Firestore within the `DataGridSource.handlePageChange` method and construct the rows for the relevant page. We have attached a simple sample that demonstrates how to load data from Firestore into a Paginated DataGrid. Please refer to the attached sample for a more comprehensive understanding of the process.

Regarding: Second problem is every time add/edit/delete, the data in firebase need to update synchronize.

We have already published a Knowledge Base (KB) document that provides step-by-step instructions how to load data from Firestore to Flutter DataGrid along with the CRUD operations. Please check the following KB document for more information.


KB document: https://support.syncfusion.com/kb/article/11968/how-to-load-data-from-firestore-to-flutter-datatable-sfdatagrid

Regards,

Tamilarasan


Attachment: sample_31008221.zip

Loader.
Up arrow icon