Skip to content

Commit

Permalink
fix: 4301 - new "up-to-date" provider for product list (#4321)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
monsieurtanuki committed Jul 16, 2023
1 parent b03f60b commit d9e49df
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 74 deletions.
28 changes: 28 additions & 0 deletions packages/smooth_app/lib/data_models/up_to_date_interest.dart
Original file line number Diff line number Diff line change
@@ -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<String, int> _interestCounts = <String, int>{};

/// 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);
}
Original file line number Diff line number Diff line change
@@ -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<T extends StatefulWidget> on State<T> {
/// 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<LocalDatabase>()`.
void refreshUpToDate() {
final List<String> barcodes =
_localDatabase.upToDateProductList.getLocalUpToDate(_productList);
_productList.set(barcodes);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, List<String>> _barcodes = <String, List<String>>{};

/// 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<String> barcodes,
) {
if (!_interest.containsKey(key)) {
return;
}
_barcodes[key] = List<String>.from(barcodes); // need to copy
}

/// Returns the latest barcodes.
List<String> getLocalUpToDate(final ProductList productList) =>
_barcodes[_getKey(productList)] ?? <String>[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<String, int> _interestingBarcodes = <String, int>{};
final UpToDateInterest _interest = UpToDateInterest();

/// Returns true if at least one barcode was refreshed after the [timestamp].
bool needsRefresh(final int? latestTimestamp, final List<String> barcodes) {
Expand All @@ -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.
Expand All @@ -82,12 +78,12 @@ class UpToDateProductProvider {
final Iterable<Product> 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);
}
Expand Down
38 changes: 26 additions & 12 deletions packages/smooth_app/lib/database/dao_product_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int?> getTimestamp(final ProductList productList) async =>
(await _get(productList))?.timestamp;
Expand All @@ -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()))}';
Expand Down Expand Up @@ -126,15 +134,21 @@ class DaoProductList extends AbstractDao {
throw Exception('Unknown product list type: "$value" from "$key"');
}

Future<void> _put(final String key, final _BarcodeList barcodeList) async =>
_getBox().put(key, barcodeList);
Future<void> _put(final String key, final _BarcodeList barcodeList) async {
await _getBox().put(key, barcodeList);
localDatabase.upToDateProductList.setLocalUpToDate(
key,
barcodeList.barcodes,
);
}

Future<void> put(final ProductList productList) async =>
_put(_getKey(productList), _BarcodeList.fromProductList(productList));
_put(getKey(productList), _BarcodeList.fromProductList(productList));

Future<bool> delete(final ProductList productList) async {
final LazyBox<_BarcodeList> box = _getBox();
final String key = _getKey(productList);
final String key = getKey(productList);
localDatabase.upToDateProductList.setLocalUpToDate(key, <String>[]);
if (!box.containsKey(key)) {
return false;
}
Expand Down Expand Up @@ -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<void> clear(final ProductList productList) async {
final _BarcodeList newList = _BarcodeList.now(<String>[]);
await _put(_getKey(productList), newList);
await _put(getKey(productList), newList);
}

/// Adds or removes a barcode within a product list (depending on [include])
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<ProductList> rename(
Expand All @@ -259,7 +273,7 @@ class DaoProductList extends AbstractDao {
final ProductList newList = ProductList.user(newName);
final _BarcodeList list =
await _get(initialList) ?? _BarcodeList.now(<String>[]);
await _put(_getKey(newList), list);
await _put(getKey(newList), list);
await delete(initialList);
await get(newList);
return newList;
Expand Down
5 changes: 5 additions & 0 deletions packages/smooth_app/lib/database/local_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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() {
Expand Down

0 comments on commit d9e49df

Please sign in to comment.