From ca21f48dab8301a46d4620d4bf646015a3d1e9b9 Mon Sep 17 00:00:00 2001 From: Sai Naw Wun Date: Fri, 13 Nov 2020 02:38:16 +0630 Subject: [PATCH] add pagination --- lib/pages/main/model/base_model.dart | 4 + lib/pages/package/model/package_model.dart | 166 +++++++++++---------- lib/pages/package/package_list.dart | 60 ++------ lib/pages/processing/processing_list.dart | 45 +++--- lib/pages/receiving/receiving_list.dart | 65 ++++---- lib/pagination/paginator_listener.dart | 163 ++++++++++++++++++++ lib/pagination/paginator_listview.dart | 99 ++++++++++++ 7 files changed, 412 insertions(+), 190 deletions(-) create mode 100644 lib/pagination/paginator_listener.dart create mode 100644 lib/pagination/paginator_listview.dart diff --git a/lib/pages/main/model/base_model.dart b/lib/pages/main/model/base_model.dart index ac8b8b5..a0b82d0 100644 --- a/lib/pages/main/model/base_model.dart +++ b/lib/pages/main/model/base_model.dart @@ -20,6 +20,10 @@ abstract class BaseModel extends ChangeNotifier { this.setting = setting; } + void notify() { + notifyListeners(); + } + void logout() {} // request makes http request diff --git a/lib/pages/package/model/package_model.dart b/lib/pages/package/model/package_model.dart index 8d44707..2dd4fd1 100644 --- a/lib/pages/package/model/package_model.dart +++ b/lib/pages/package/model/package_model.dart @@ -8,116 +8,126 @@ import 'package:fcs/domain/entities/package.dart'; import 'package:fcs/domain/entities/user.dart'; import 'package:fcs/domain/vo/delivery_address.dart'; import 'package:fcs/helpers/firebase_helper.dart'; -import 'package:fcs/helpers/paginator.dart'; import 'package:fcs/pages/main/model/base_model.dart'; +import 'package:fcs/pagination/paginator_listener.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as Path; class PackageModel extends BaseModel { final log = Logger('PackageModel'); - StreamSubscription listener; + PaginatorListener packages; + PaginatorListener customerPackages; + PaginatorListener activePackages; - List get packages => _menuSelectedIndex == 1 - ? _packages - : List.from(_delivered.values); - - List _packages = []; - - Paginator _delivered; bool isLoading = false; int _menuSelectedIndex = 1; set menuSelectedIndex(int index) { _menuSelectedIndex = index; + + _loadPackages(_menuSelectedIndex == 2); + _loadCustomerPackages(_menuSelectedIndex == 2); + notifyListeners(); } get menuSelectedIndex => _menuSelectedIndex; - initData(bool forCustomer) { + void privilegeChanged() { + if (user != null) { + _initData(); + } + } + + Future _initData() async { logout(); _menuSelectedIndex = 1; - _loadPackages(forCustomer); - _delivered = _getDelivered(forCustomer); - _delivered.load(); + packages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + customerPackages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + activePackages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + + _loadPackages(_menuSelectedIndex == 2); + _loadCustomerPackages(_menuSelectedIndex == 2); + _loadActivePackages(); } @override logout() async { - if (_delivered != null) _delivered.close(); - if (listener != null) await listener.cancel(); - _packages = []; + if (customerPackages != null) customerPackages.close(); + if (packages != null) packages.close(); + if (activePackages != null) activePackages.close(); } - Future loadMore({bool isCustomer}) async { - if (_delivered.ended || menuSelectedIndex == 1) - return; // when delivered menu is not selected return - isLoading = true; - notifyListeners(); - await _delivered.load(onFinished: () { - isLoading = false; - notifyListeners(); - }); - } - - Future refresh({bool isCustomer}) async { - if (menuSelectedIndex == 1) - return; // when delivered menu is not selected return - await _delivered.refresh(onFinished: () { - notifyListeners(); - }); - } - - Paginator _getDelivered(bool isCustomer) { - if (!isCustomer) { - if (user == null || - !((user.hasPackages() || - user.hasReceiving() || - user.hasProcessing()))) throw "No privilege"; - } - var pageQuery = Firestore.instance - .collection("/$packages_collection") - .where("is_delivered", isEqualTo: true) - .where("is_deleted", isEqualTo: false); - if (isCustomer) { - pageQuery = pageQuery.where("user_id", isEqualTo: user.id); - } - pageQuery = pageQuery.orderBy("status_date", descending: true); - var paginator = new Paginator(pageQuery, rowPerLoad: 20, toObj: (data, id) { - return Package.fromMap(data, id); - }); - return paginator; - } - - Future _loadPackages(bool forCustomer) async { + Future _loadPackages(bool isDelivered) async { if (user == null) return; - if (!forCustomer && - !((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) + if (!((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) return; String path = "/$packages_collection"; - if (listener != null) listener.cancel(); - _packages = []; try { - var q = Firestore.instance - .collection("$path") - .where("is_delivered", isEqualTo: false) - .where("is_deleted", isEqualTo: false); + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered); - if (forCustomer) { - q = q.where("user_id", isEqualTo: user.id); - } - q = q.orderBy("update_time", descending: true); - listener = q.snapshots().listen((QuerySnapshot snapshot) { - _packages.clear(); - _packages = snapshot.documents.map((documentSnapshot) { - var package = Package.fromMap( - documentSnapshot.data, documentSnapshot.documentID); - return package; - }).toList(); - notifyListeners(); - }); + pageQuery = pageQuery.orderBy("update_time", descending: true); + packages.refresh(listeningQuery: listenerQuery, pageQuery: pageQuery); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadCustomerPackages(bool isDelivered) async { + if (user == null) return; + String path = "/$packages_collection"; + + try { + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered) + .where("user_id", isEqualTo: user.id); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered) + .where("user_id", isEqualTo: user.id) + .orderBy("update_time", descending: true); + + customerPackages.refresh( + listeningQuery: listenerQuery, pageQuery: pageQuery); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadActivePackages() async { + if (user == null) return; + if (!((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) + return; + String path = "/$packages_collection"; + + try { + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: false); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: false); + + pageQuery = pageQuery.orderBy("update_time", descending: true); + activePackages.refresh( + listeningQuery: listenerQuery, pageQuery: pageQuery); } catch (e) { log.warning("Error!! $e"); } diff --git a/lib/pages/package/package_list.dart b/lib/pages/package/package_list.dart index 6d3c3f6..c28ce22 100644 --- a/lib/pages/package/package_list.dart +++ b/lib/pages/package/package_list.dart @@ -8,6 +8,7 @@ import 'package:fcs/pages/widgets/local_popup_menu_button.dart'; import 'package:fcs/pages/widgets/local_popupmenu.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,26 +23,18 @@ class PackageList extends StatefulWidget { class _PackageListState extends State { bool _isLoading = false; - var _controller = ScrollController(); @override void initState() { super.initState(); - - _controller.addListener(() async { - if (_controller.position.pixels == _controller.position.maxScrollExtent) { - Provider.of(context, listen: false) - .loadMore(isCustomer: widget.forCustomer); - } - }); - Provider.of(context, listen: false) - .initData(widget.forCustomer); } @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - var packages = packageModel.packages; + var packages = widget.forCustomer + ? packageModel.customerPackages + : packageModel.packages; final popupMenu = LocalPopupMenuButton( popmenus: [ @@ -90,43 +83,14 @@ class _PackageListState extends State { popupMenu ], ), - body: Column( - children: [ - Expanded( - child: RefreshIndicator( - child: ListView.separated( - controller: _controller, - separatorBuilder: (context, index) => Divider( - color: Colors.grey, - height: 1, - ), - scrollDirection: Axis.vertical, - itemCount: packages.length, - itemBuilder: (BuildContext context, int index) { - return PackageListRow( - key: ValueKey(packages[index].id), - package: packages[index], - isCustomer: widget.forCustomer, - ); - }), - onRefresh: () => - packageModel.refresh(isCustomer: widget.forCustomer), - ), - ), - packageModel.isLoading - ? Container( - padding: EdgeInsets.all(8), - color: primaryColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Loading...", - style: TextStyle(color: Colors.white)), - ], - ), - ) - : Container(), - ], + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => PackageListRow( + key: ValueKey(p.id), + package: p, + isCustomer: widget.forCustomer, + ), + color: primaryColor, )), ); } diff --git a/lib/pages/processing/processing_list.dart b/lib/pages/processing/processing_list.dart index 9a1d5f0..6ab3497 100644 --- a/lib/pages/processing/processing_list.dart +++ b/lib/pages/processing/processing_list.dart @@ -5,6 +5,7 @@ import 'package:fcs/pages/package/model/package_model.dart'; import 'package:fcs/pages/package_search/package_serach.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -33,7 +34,7 @@ class _ProcessingListState extends State { @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - bool isCustomer = context.select((MainModel m) => m.isCustomer()); + var packages = packageModel.activePackages; return LocalProgress( inAsyncCall: _isLoading, @@ -52,33 +53,25 @@ class _ProcessingListState extends State { color: Colors.white, ), actions: [ - isCustomer - ? Container() - : IconButton( - icon: Icon( - Icons.search, - color: Colors.white, - ), - iconSize: 30, - onPressed: () => searchPackage(context, - callbackPackageSelect: _searchCallback), - ), + IconButton( + icon: Icon( + Icons.search, + color: Colors.white, + ), + iconSize: 30, + onPressed: () => searchPackage(context, + callbackPackageSelect: _searchCallback), + ), ], ), - body: new ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Colors.black, - height: 1, - ), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: packageModel.packages.length, - itemBuilder: (BuildContext context, int index) { - return ProcessingListRow( - key: ValueKey(packageModel.packages[index].id), - package: packageModel.packages[index], - ); - })), + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => ProcessingListRow( + key: ValueKey(p.id), + package: p, + ), + color: primaryColor, + )), ); } diff --git a/lib/pages/receiving/receiving_list.dart b/lib/pages/receiving/receiving_list.dart index 4d6ecee..d8e9777 100644 --- a/lib/pages/receiving/receiving_list.dart +++ b/lib/pages/receiving/receiving_list.dart @@ -6,6 +6,7 @@ import 'package:fcs/pages/package_search/package_serach.dart'; import 'package:fcs/pages/widgets/bottom_up_page_route.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -25,7 +26,6 @@ class _ReceivingListState extends State { @override void initState() { super.initState(); - Provider.of(context, listen: false).initData(false); } @override @@ -36,7 +36,7 @@ class _ReceivingListState extends State { @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - bool isCustomer = context.select((MainModel m) => m.isCustomer()); + var packages = packageModel.activePackages; return LocalProgress( inAsyncCall: _isLoading, @@ -55,44 +55,33 @@ class _ReceivingListState extends State { color: Colors.white, ), actions: [ - isCustomer - ? Container() - : IconButton( - icon: Icon( - Icons.search, - color: Colors.white, - ), - iconSize: 30, - onPressed: () => searchPackage(context, - callbackPackageSelect: _searchCallback), - ), + IconButton( + icon: Icon( + Icons.search, + color: Colors.white, + ), + iconSize: 30, + onPressed: () => searchPackage(context, + callbackPackageSelect: _searchCallback), + ), ], ), - floatingActionButton: isCustomer - ? Container() - : FloatingActionButton.extended( - onPressed: () { - _newReceiving(); - }, - icon: Icon(Icons.add), - label: - LocalText(context, "receiving.new", color: Colors.white), - backgroundColor: primaryColor, - ), - body: new ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Colors.black, - height: 1, - ), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: packageModel.packages.length, - itemBuilder: (BuildContext context, int index) { - return ReceivingListRow( - key: ValueKey(packageModel.packages[index].id), - package: packageModel.packages[index], - ); - })), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + _newReceiving(); + }, + icon: Icon(Icons.add), + label: LocalText(context, "receiving.new", color: Colors.white), + backgroundColor: primaryColor, + ), + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => ReceivingListRow( + key: ValueKey(p.id), + package: p, + ), + color: primaryColor, + )), ); } diff --git a/lib/pagination/paginator_listener.dart b/lib/pagination/paginator_listener.dart new file mode 100644 index 0000000..9907930 --- /dev/null +++ b/lib/pagination/paginator_listener.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/pages/widgets/callbacks.dart'; +import 'package:logging/logging.dart'; + +typedef ToObj = Function(Map data, String id); + +/* + * PaginatorListener load data in page + * and listen to document change based on 'update_time' and 'delete_time' fields + * of the document + */ +class PaginatorListener { + final log = Logger('PaginatorListener'); + + List ids = []; + List data = []; + DocumentSnapshot prev; + int rowPerLoad = 10; + bool ended = false; + bool isLoading = false; + bool insertNewByListener = false; + ToObj toObj; + CallBack onChange; + + StreamSubscription listener; + Query listeningQuery; + Query pageQuery; + + PaginatorListener(this.toObj, + {this.onChange, this.rowPerLoad = 10, this.insertNewByListener = false}); + + Future refresh({Query listeningQuery, Query pageQuery}) { + this.listeningQuery = listeningQuery ?? this.listeningQuery; + this.pageQuery = pageQuery ?? this.pageQuery; + _clearState(); + _initListener(); + return _load(); + } + + void _clearState() { + prev = null; + ids = []; + data = []; + ended = false; + isLoading = false; + if (listener != null) listener.cancel(); + listener = null; + } + + void close() { + _clearState(); + } + + final String updateTimeField = 'update_time'; + final String deleteTimeField = 'delete_time'; + final String isDeletedField = 'is_deleted'; + void _initListener() { + Query _query = + listeningQuery.orderBy(updateTimeField, descending: true).limit(1); + _query.getDocuments(source: Source.server).then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + int updateTime = 0; + if (count == 1) { + updateTime = snapshot.documents[0].data[updateTimeField]; + } + + Query _queryListener = listeningQuery + .where(updateTimeField, isGreaterThan: updateTime) + .orderBy(updateTimeField, descending: true); + + if (listener != null) listener.cancel(); + listener = + _queryListener.snapshots(includeMetadataChanges: true).listen((qs) { + qs.documentChanges.forEach((c) { + switch (c.type) { + case DocumentChangeType.added: + _update(c.document.documentID, c.document.data); + break; + case DocumentChangeType.modified: + _update(c.document.documentID, c.document.data); + break; + case DocumentChangeType.removed: + _remove(c.document.documentID, c.document.data); + break; + default: + } + if (onChange != null) onChange(); + }); + }); + }); + } + + void _update(String id, Map map) { + T t = toObj(map, id); + if (ids.contains(id)) { + var deleted = map[deleteTimeField]; + var isDeleted = map[isDeletedField]; + if ((deleted ?? 0) > 0 || (isDeleted ?? false)) { + data.removeAt(ids.indexOf(id)); + ids.remove(id); + } else { + data.removeAt(ids.indexOf(id)); + data.insert(ids.indexOf(id), t); + } + } else if (insertNewByListener) { + data.insert(0, t); + ids.insert(0, id); + } + } + + void _add(String id, Map map) { + T t = toObj(map, id); + if (!ids.contains(id)) { + data.add(t); + ids.add(id); + } + } + + void _remove(String id, Map map) { + if (ids.contains(id)) { + data.removeAt(ids.indexOf(id)); + ids.remove(id); + } + } + + Future loadMore() { + return this._load(); + } + + Future _load({CallBack onStarted, CallBack onFinished}) async { + if (ended) return ended; + Query _query = + prev != null ? pageQuery.startAfterDocument(prev) : pageQuery; + try { + isLoading = true; + if (onStarted != null) { + onStarted(); + } + if (onChange != null) onChange(); + await _query + .where(isDeletedField, isEqualTo: false) + .limit(rowPerLoad) + .getDocuments(source: Source.server) + .then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + ended = count < rowPerLoad; + prev = count > 0 ? snapshot.documents[count - 1] : prev; + snapshot.documents.forEach((e) { + _add(e.documentID, e.data); + }); + }); + } catch (e) { + log.warning("Error!! $e"); + } finally { + isLoading = false; + if (onFinished != null) onFinished(); + if (onChange != null) onChange(); + } + return ended; + } +} diff --git a/lib/pagination/paginator_listview.dart b/lib/pagination/paginator_listview.dart new file mode 100644 index 0000000..f0a7d39 --- /dev/null +++ b/lib/pagination/paginator_listview.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'paginator_listener.dart'; + +typedef RowBuilder = Widget Function(dynamic); +typedef OnScroll = void Function(bool down); + +class PaginatorListView extends StatelessWidget { + final PaginatorListener paginatorListener; + final RowBuilder rowBuilder; + final OnScroll onScroll; + final ScrollController _scrollController; + final Color color; + + PaginatorListView( + {Key key, + this.paginatorListener, + this.rowBuilder, + this.onScroll, + this.color = Colors.blueAccent}) + : _scrollController = ScrollController(), + assert(paginatorListener != null), + assert(rowBuilder != null), + super(key: key) { + _scrollController.addListener(() async { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + paginatorListener.loadMore(); + } + if (onScroll != null) { + var down = _scrollController.position.userScrollDirection == + ScrollDirection.forward; + onScroll(down); + } + }); + } + + @override + Widget build(BuildContext context) { + bool ended = paginatorListener.ended; + int count = paginatorListener.data.length; + if (ended) count++; + + return Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () { + return paginatorListener.refresh(); + }, + child: ListView.separated( + separatorBuilder: (context, index) => + Divider(height: 1, color: Colors.black), + controller: _scrollController, + scrollDirection: Axis.vertical, + physics: const AlwaysScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: count, + itemBuilder: (BuildContext context, int index) { + if (ended && index == count - 1) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center(child: Text("No more data")), + ); + } + T t = paginatorListener.data[index]; + return rowBuilder(t); + }), + ), + ), + paginatorListener.isLoading ? _loadingRow() : Container() + ], + ); + } + + Widget _loadingRow() { + return Container( + padding: EdgeInsets.all(8), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(color)), + Text( + "Loading...", + style: TextStyle(color: color), + ) + ], + ), + ], + ), + ); + } +}