Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 4301 - new "up-to-date" provider for product list #4321

Merged
merged 1 commit into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading