From e199713532fce899d7a605722136e8b9862f14bf Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 16 Jul 2023 16:03:57 +0200 Subject: [PATCH] fix: 4301 - new "up-to-date" provider for product list New files: * `up_to_date_interest.dart`: Management of the interest for a key. * `up_to_date_product_list_mixin.dart`: Provides the most up-to-date local product list data for a StatefulWidget. * `up_to_date_product_list_provider.dart`: Provider that reflects the latest barcode lists on ProductLists. Impacted files: * `dao_product_list.dart`: made public method `getKey`; refreshes the provider * `local_database.dart`: added a new `UpToDateProductListProvider` * `product_list_page.dart`: now extends `UpToDateProductListMixin * `up_to_date_product_provider.dart`: refactored using a `UpToDateInterest` * `user_preferences_account.dart`: removed redundant access to product list page --- .../lib/data_models/up_to_date_interest.dart | 28 ++++++++++ .../up_to_date_product_list_mixin.dart | 55 +++++++++++++++++++ .../up_to_date_product_list_provider.dart | 54 ++++++++++++++++++ .../up_to_date_product_provider.dart | 24 ++++---- .../lib/database/dao_product_list.dart | 38 +++++++++---- .../lib/database/local_database.dart | 5 ++ .../preferences/user_preferences_account.dart | 44 --------------- .../product/common/product_list_page.dart | 9 +-- 8 files changed, 183 insertions(+), 74 deletions(-) create mode 100644 packages/smooth_app/lib/data_models/up_to_date_interest.dart create mode 100644 packages/smooth_app/lib/data_models/up_to_date_product_list_mixin.dart create mode 100644 packages/smooth_app/lib/data_models/up_to_date_product_list_provider.dart diff --git a/packages/smooth_app/lib/data_models/up_to_date_interest.dart b/packages/smooth_app/lib/data_models/up_to_date_interest.dart new file mode 100644 index 00000000000..d107fde7ee3 --- /dev/null +++ b/packages/smooth_app/lib/data_models/up_to_date_interest.dart @@ -0,0 +1,28 @@ +/// Management of the interest for a key. +class UpToDateInterest { + /// Number of time an interest was shown for a given key. + final Map _interestCounts = {}; + + /// Shows an interest for a key. + void add(final String key) { + final int result = (_interestCounts[key] ?? 0) + 1; + _interestCounts[key] = result; + } + + /// Loses an interest for a key. + /// + /// Returns true if completely lost interest. + bool remove(final String key) { + final int result = (_interestCounts[key] ?? 0) - 1; + if (result <= 0) { + _interestCounts.remove(key); + return true; + } + _interestCounts[key] = result; + return false; + } + + bool get isEmpty => _interestCounts.isEmpty; + + bool containsKey(final String key) => _interestCounts.containsKey(key); +} diff --git a/packages/smooth_app/lib/data_models/up_to_date_product_list_mixin.dart b/packages/smooth_app/lib/data_models/up_to_date_product_list_mixin.dart new file mode 100644 index 00000000000..eda77dc288a --- /dev/null +++ b/packages/smooth_app/lib/data_models/up_to_date_product_list_mixin.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/data_models/product_list.dart'; +import 'package:smooth_app/database/dao_product_list.dart'; +import 'package:smooth_app/database/local_database.dart'; + +/// Provides the most up-to-date local product list data for a StatefulWidget. +@optionalTypeArgs +mixin UpToDateProductListMixin on State { + /// To be used in the `initState` method. + void initUpToDate( + final ProductList initialProductList, + final LocalDatabase localDatabase, + ) { + _productList = initialProductList; + _localDatabase = localDatabase; + _localDatabase.upToDateProductList.showInterest(initialProductList); + _localDatabase.upToDateProductList.setLocalUpToDate( + DaoProductList.getKey(_productList), + _productList.barcodes, + ); + } + + late final LocalDatabase _localDatabase; + + late ProductList _productList; + + ProductList get productList => _productList; + + set productList(final ProductList productList) { + final ProductList previous = _productList; + _productList = productList; + _localDatabase.upToDateProductList.showInterest(_productList); + _localDatabase.upToDateProductList.loseInterest(previous); + _localDatabase.upToDateProductList.setLocalUpToDate( + DaoProductList.getKey(_productList), + _productList.barcodes, + ); + } + + @override + void dispose() { + _localDatabase.upToDateProductList.loseInterest(_productList); + super.dispose(); + } + + /// Refreshes [upToDateProduct] with the latest available local data. + /// + /// To be used in the `build` method, after a call to + /// `context.watch()`. + void refreshUpToDate() { + final List barcodes = + _localDatabase.upToDateProductList.getLocalUpToDate(_productList); + _productList.set(barcodes); + } +} diff --git a/packages/smooth_app/lib/data_models/up_to_date_product_list_provider.dart b/packages/smooth_app/lib/data_models/up_to_date_product_list_provider.dart new file mode 100644 index 00000000000..422c54a86a5 --- /dev/null +++ b/packages/smooth_app/lib/data_models/up_to_date_product_list_provider.dart @@ -0,0 +1,54 @@ +import 'package:smooth_app/data_models/product_list.dart'; +import 'package:smooth_app/data_models/up_to_date_interest.dart'; +import 'package:smooth_app/database/dao_product_list.dart'; +import 'package:smooth_app/database/local_database.dart'; + +/// Provider that reflects the latest barcode lists on [ProductList]s. +class UpToDateProductListProvider { + UpToDateProductListProvider(this.localDatabase); + + final LocalDatabase localDatabase; + + /// Product lists currently displayed in the app. + /// + /// We need to know which product lists are "interesting" because we need to + /// cache barcode lists in memory for instant access. And we should cache only + /// them, because we cannot cache all product lists in memory. + final UpToDateInterest _interest = UpToDateInterest(); + + final Map> _barcodes = >{}; + + /// Shows an interest for a product list. + /// + /// Typically, to be used by a widget in `initState`. + void showInterest(final ProductList productList) => + _interest.add(_getKey(productList)); + + /// Loses interest for a product list. + /// + /// Typically, to be used by a widget in `dispose`. + void loseInterest(final ProductList productList) { + final String key = _getKey(productList); + if (!_interest.remove(key)) { + return; + } + _barcodes.remove(key); + } + + String _getKey(final ProductList productList) => + DaoProductList.getKey(productList); + + void setLocalUpToDate( + final String key, + final List barcodes, + ) { + if (!_interest.containsKey(key)) { + return; + } + _barcodes[key] = List.from(barcodes); // need to copy + } + + /// Returns the latest barcodes. + List getLocalUpToDate(final ProductList productList) => + _barcodes[_getKey(productList)] ?? []; +} diff --git a/packages/smooth_app/lib/data_models/up_to_date_product_provider.dart b/packages/smooth_app/lib/data_models/up_to_date_product_provider.dart index 4170b737997..f8a38e3e4d8 100644 --- a/packages/smooth_app/lib/data_models/up_to_date_product_provider.dart +++ b/packages/smooth_app/lib/data_models/up_to_date_product_provider.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/data_models/up_to_date_changes.dart'; +import 'package:smooth_app/data_models/up_to_date_interest.dart'; import 'package:smooth_app/database/dao_transient_operation.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -26,7 +27,7 @@ class UpToDateProductProvider { /// We need to know which barcodes are "interesting" because we need to cache /// products in memory for instant access. And we should cache only them, /// because we cannot cache all products in memory. - final Map _interestingBarcodes = {}; + final UpToDateInterest _interest = UpToDateInterest(); /// Returns true if at least one barcode was refreshed after the [timestamp]. bool needsRefresh(final int? latestTimestamp, final List barcodes) { @@ -46,23 +47,18 @@ class UpToDateProductProvider { /// Shows an interest for a barcode. /// /// Typically, to be used by a widget in `initState`. - void showInterest(final String barcode) { - final int result = (_interestingBarcodes[barcode] ?? 0) + 1; - _interestingBarcodes[barcode] = result; - } + void showInterest(final String barcode) => _interest.add(barcode); /// Loses interest for a barcode. /// /// Typically, to be used by a widget in `dispose`. void loseInterest(final String barcode) { - final int result = (_interestingBarcodes[barcode] ?? 0) - 1; - if (result <= 0) { - _interestingBarcodes.remove(barcode); - _latestDownloadedProducts.remove(barcode); - _timestamps.remove(barcode); - } else { - _interestingBarcodes[barcode] = result; + final bool lostInterest = _interest.remove(barcode); + if (!lostInterest) { + return; } + _latestDownloadedProducts.remove(barcode); + _timestamps.remove(barcode); } /// Typical use-case: a product page is refreshed through a pull-gesture. @@ -82,12 +78,12 @@ class UpToDateProductProvider { final Iterable products, { final bool notify = true, }) { - if (_interestingBarcodes.isEmpty) { + if (_interest.isEmpty) { return; } bool atLeastOne = false; for (final Product product in products) { - if (_interestingBarcodes.containsKey(product.barcode)) { + if (_interest.containsKey(product.barcode!)) { atLeastOne = true; setLatestDownloadedProduct(product, notify: false); } diff --git a/packages/smooth_app/lib/database/dao_product_list.dart b/packages/smooth_app/lib/database/dao_product_list.dart index 047bb81bb1a..134408ed586 100644 --- a/packages/smooth_app/lib/database/dao_product_list.dart +++ b/packages/smooth_app/lib/database/dao_product_list.dart @@ -84,8 +84,16 @@ class DaoProductList extends AbstractDao { LazyBox<_BarcodeList> _getBox() => Hive.lazyBox<_BarcodeList>(_hiveBoxName); - Future<_BarcodeList?> _get(final ProductList productList) => - _getBox().get(_getKey(productList)); + Future<_BarcodeList?> _get(final ProductList productList) async { + final _BarcodeList? result = await _getBox().get(getKey(productList)); + if (result != null) { + localDatabase.upToDateProductList.setLocalUpToDate( + getKey(productList), + result.barcodes, + ); + } + return result; + } Future getTimestamp(final ProductList productList) async => (await _get(productList))?.timestamp; @@ -95,7 +103,7 @@ class DaoProductList extends AbstractDao { // Encoding the parameter part in base64 makes us safe regarding ASCII. // As it's a list of keywords, there's a fairly high probability // that we'll be under the 255 character length. - static String _getKey(final ProductList productList) => + static String getKey(final ProductList productList) => '${productList.listType.key}' '$_keySeparator' '${base64.encode(utf8.encode(productList.getParametersKey()))}'; @@ -126,15 +134,21 @@ class DaoProductList extends AbstractDao { throw Exception('Unknown product list type: "$value" from "$key"'); } - Future _put(final String key, final _BarcodeList barcodeList) async => - _getBox().put(key, barcodeList); + Future _put(final String key, final _BarcodeList barcodeList) async { + await _getBox().put(key, barcodeList); + localDatabase.upToDateProductList.setLocalUpToDate( + key, + barcodeList.barcodes, + ); + } Future put(final ProductList productList) async => - _put(_getKey(productList), _BarcodeList.fromProductList(productList)); + _put(getKey(productList), _BarcodeList.fromProductList(productList)); Future delete(final ProductList productList) async { final LazyBox<_BarcodeList> box = _getBox(); - final String key = _getKey(productList); + final String key = getKey(productList); + localDatabase.upToDateProductList.setLocalUpToDate(key, []); if (!box.containsKey(key)) { return false; } @@ -182,12 +196,12 @@ class DaoProductList extends AbstractDao { barcodes.remove(barcode); // removes a potential duplicate barcodes.add(barcode); final _BarcodeList newList = _BarcodeList.now(barcodes); - await _put(_getKey(productList), newList); + await _put(getKey(productList), newList); } Future clear(final ProductList productList) async { final _BarcodeList newList = _BarcodeList.now([]); - await _put(_getKey(productList), newList); + await _put(getKey(productList), newList); } /// Adds or removes a barcode within a product list (depending on [include]) @@ -217,7 +231,7 @@ class DaoProductList extends AbstractDao { barcodes.add(barcode); } final _BarcodeList newList = _BarcodeList.now(barcodes); - await _put(_getKey(productList), newList); + await _put(getKey(productList), newList); return true; } @@ -249,7 +263,7 @@ class DaoProductList extends AbstractDao { } final _BarcodeList newList = _BarcodeList.now(allBarcodes); - await _put(_getKey(productList), newList); + await _put(getKey(productList), newList); } Future rename( @@ -259,7 +273,7 @@ class DaoProductList extends AbstractDao { final ProductList newList = ProductList.user(newName); final _BarcodeList list = await _get(initialList) ?? _BarcodeList.now([]); - await _put(_getKey(newList), list); + await _put(getKey(newList), list); await delete(initialList); await get(newList); return newList; diff --git a/packages/smooth_app/lib/database/local_database.dart b/packages/smooth_app/lib/database/local_database.dart index cab27cba19b..21db1927c71 100644 --- a/packages/smooth_app/lib/database/local_database.dart +++ b/packages/smooth_app/lib/database/local_database.dart @@ -6,6 +6,7 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:smooth_app/background/background_task_manager.dart'; +import 'package:smooth_app/data_models/up_to_date_product_list_provider.dart'; import 'package:smooth_app/data_models/up_to_date_product_provider.dart'; import 'package:smooth_app/database/abstract_dao.dart'; import 'package:smooth_app/database/dao_hive_product.dart'; @@ -25,14 +26,18 @@ import 'package:sqflite/sqflite.dart'; class LocalDatabase extends ChangeNotifier { LocalDatabase._(final Database database) : _database = database { _upToDateProductProvider = UpToDateProductProvider(this); + _upToDateProductListProvider = UpToDateProductListProvider(this); } final Database _database; late final UpToDateProductProvider _upToDateProductProvider; + late final UpToDateProductListProvider _upToDateProductListProvider; Database get database => _database; UpToDateProductProvider get upToDate => _upToDateProductProvider; + UpToDateProductListProvider get upToDateProductList => + _upToDateProductListProvider; @override void notifyListeners() { diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 7ec97cf9cec..8071f45e517 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; -import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; -import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; @@ -17,7 +15,6 @@ import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart'; import 'package:smooth_app/pages/preferences/account_deletion_webview.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; -import 'package:smooth_app/pages/product/common/product_list_page.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/user_management/login_page.dart'; import 'package:smooth_app/query/paged_product_query.dart'; @@ -290,12 +287,6 @@ class _UserPreferencesPageState extends State { context: context, localDatabase: localDatabase, ), - _buildProductLocalTile( - productList: ProductList.scanHistory(), - iconData: Icons.history, - context: context, - localDatabase: localDatabase, - ), _getListTile( appLocalizations.view_profile, () async => LaunchUrlHelper.launchURL( @@ -399,14 +390,6 @@ class _UserPreferencesPageState extends State { } } - Future _getMyLocalCount( - final ProductList productList, - final LocalDatabase localDatabase, - ) async { - await DaoProductList(localDatabase).get(productList); - return productList.barcodes.length; - } - Widget _buildProductQueryTile({ required final PagedProductQuery productQuery, required final String title, @@ -428,33 +411,6 @@ class _UserPreferencesPageState extends State { myCount: myCount, ); - Widget _buildProductLocalTile({ - required final ProductList productList, - required final IconData iconData, - required final BuildContext context, - required final LocalDatabase localDatabase, - }) => - _getListTile( - ProductQueryPageHelper.getProductListLabel( - productList, - AppLocalizations.of(context), - ), - () async { - await DaoProductList(localDatabase).get(productList); - if (!mounted) { - return; - } - await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ProductListPage(productList), - ), - ); - }, - iconData, - myCount: _getMyLocalCount(productList, localDatabase), - ); - Widget _getListTile( final String title, final VoidCallback onTap, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index ab995840d41..1b112a9e7e2 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -7,6 +7,7 @@ import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/product_list.dart'; +import 'package:smooth_app/data_models/up_to_date_product_list_mixin.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -38,8 +39,7 @@ class ProductListPage extends StatefulWidget { } class _ProductListPageState extends State - with TraceableClientMixin { - late ProductList productList; + with TraceableClientMixin, UpToDateProductListMixin { final Set _selectedBarcodes = {}; bool _selectionMode = false; @@ -52,7 +52,7 @@ class _ProductListPageState extends State @override void initState() { super.initState(); - productList = widget.productList; + initUpToDate(widget.productList, context.read()); } final ProductListPopupItem _rename = ProductListPopupRename(); @@ -81,6 +81,7 @@ class _ProductListPageState extends State final DaoProductList daoProductList = DaoProductList(localDatabase); final ThemeData themeData = Theme.of(context); final AppLocalizations appLocalizations = AppLocalizations.of(context); + refreshUpToDate(); final List products = productList.getList(); final bool dismissible; switch (productList.listType) { @@ -140,7 +141,7 @@ class _ProductListPageState extends State return; } if (context.mounted) { - await DaoProductList(localDatabase).get(selected); + await daoProductList.get(selected); if (context.mounted) { setState(() => productList = selected); }