Skip to content

Commit

Permalink
feat: #1333 - now the back-end results are paged. (#1437)
Browse files Browse the repository at this point in the history
New files:
* `paged_product_query.dart`: Paged product query (with pageSize and pageNumber).
* `partial_product_list.dart`: List of Products out of partial results (e.g. paged results).

Impacted files:
* `category_product_query.dart`: now extending new class `PagedProductQuery`.
* `dao_product_list.dart`: now storing "total size" of paged results; now including page size and page number in the primary key; now returning a bool for method `delete`.
* `database_product_list_supplier.dart`: now loads paged results page after page.
* `keywords_product_query.dart`: now extending new class `PagedProductQuery`.
* `new_product_page.dart`: unrelated fix about back icon on Android/iOS.
* `onboarding_flow_navigator.dart`: unrelated fix about back icon on Android/iOS.
* `personalized_ranking_page.dart`: refactoring.
* `Podfile.lock`: wtf
* `product_list.dart`: removed now unused product list type (pnns); added fields page size, page number and total size for paged results; refactored.
* `product_list_page.dart`: refactored.
* `product_list_supplier.dart`: now using `PagedProductQuery` and `PartialProductList`.
* `product_query_model.dart`: added a `clear` method to go back to top page; added method `loadNextPage` and `loadFromTop`.
* `product_query_page.dart`: now displaying pages results with a "load next" button; unrelated fix about back icon on Android/iOS.
* `product_query_page_helper.dart`: refactored.
* `query_product_list_supplier.dart`: now using `PagedProductQuery`.
* `scan_page_helper.dart`: refactored.
* `search_page.dart`: refactored.
* `summary_card.dart`: refactored.
  • Loading branch information
monsieurtanuki authored Apr 6, 2022
1 parent d1f82bd commit e9ec9b3
Show file tree
Hide file tree
Showing 20 changed files with 424 additions and 227 deletions.
12 changes: 11 additions & 1 deletion packages/smooth_app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ PODS:
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (1.7.1)
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.6.1)
- image_picker (0.0.1):
- Flutter
- iso_countries (0.0.1):
Expand Down Expand Up @@ -91,6 +94,7 @@ PODS:
- Sentry (~> 7.10.1)
- shared_preferences_ios (0.0.1):
- Flutter
- TOCropViewController (2.6.1)
- url_launcher_ios (0.0.1):
- Flutter

Expand All @@ -100,6 +104,7 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- google_ml_barcode_scanner (from `.symlinks/plugins/google_ml_barcode_scanner/ios`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- iso_countries (from `.symlinks/plugins/iso_countries/ios`)
- matomo_forever (from `.symlinks/plugins/matomo_forever/ios`)
Expand Down Expand Up @@ -128,6 +133,7 @@ SPEC REPOS:
- PromisesObjC
- Protobuf
- Sentry
- TOCropViewController

EXTERNAL SOURCES:
camera:
Expand All @@ -140,6 +146,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
google_ml_barcode_scanner:
:path: ".symlinks/plugins/google_ml_barcode_scanner/ios"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
iso_countries:
Expand Down Expand Up @@ -173,6 +181,7 @@ SPEC CHECKSUMS:
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 4577a4cc914a5a07c40a8a0ad0acc22080418c2d
image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98
image_picker: 541dcbb3b9cf32d87eacbd957845d8651d6c62c3
iso_countries: eb09d40f388e4c65e291e0bb36a701dfe7de6c74
matomo_forever: 7e5e5fd8f355f64979591282cad4e858fa4c9fae
Expand All @@ -191,8 +200,9 @@ SPEC CHECKSUMS:
Sentry: 7bf9bfe713692cf87812e55f0999260494ba7982
sentry_flutter: 77ccdac346608b8ce7e428e7284e7a3e4e7f4a02
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de

PODFILE CHECKSUM: e1ffd3daa5042cd516081f94f61b857c0deb822d

COCOAPODS: 1.11.2
COCOAPODS: 1.11.3
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,42 @@ import 'package:smooth_app/data_models/product_list_supplier.dart';
import 'package:smooth_app/data_models/query_product_list_supplier.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/database/product_query.dart';
import 'package:smooth_app/database/paged_product_query.dart';

/// Supplier of previous back-end results now stored in the local database.
class DatabaseProductListSupplier extends ProductListSupplier {
DatabaseProductListSupplier(
final ProductQuery productQuery,
final PagedProductQuery pagedProductQuery,
final LocalDatabase localDatabase,
final int timestamp,
) : super(productQuery, localDatabase, timestamp: timestamp);
) : super(pagedProductQuery, localDatabase, timestamp: timestamp);

/// Loads all results page after page.
@override
Future<String?> asyncLoad() async {
try {
final ProductList loadedProductList = productQuery.getProductList();
await DaoProductList(localDatabase).get(loadedProductList);
productList = loadedProductList;
return null;
// we start from page 1
ProductList productList = productQuery.getProductList();
bool first = true;
do {
// we try to get the locally saved data for the current page
await DaoProductList(localDatabase).get(productList);
if (productList.barcodes.isEmpty) {
// we found nothing
if (first) {
// we save an empty list
partialProductList.add(productList);
}
// that's it, we've just loaded all the non-empty pages we could
return null;
}
// we found something: let's add it to the partial product list
partialProductList.add(productList);
// and try again with the next page
productQuery.toNextPage();
productList = productQuery.getProductList();
first = false;
} while (true);
} catch (e) {
return e.toString();
}
Expand Down
23 changes: 23 additions & 0 deletions packages/smooth_app/lib/data_models/partial_product_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/data_models/product_list.dart';

/// List of [Product]s out of partial results (e.g. paged results).
class PartialProductList {
final List<Product> _products = <Product>[];
int _totalSize = 0;

/// Total size of the list from which this partial list is taken.
int get totalSize => _totalSize;

List<Product> getProducts() => _products;

void add(final ProductList productList) {
_products.addAll(productList.getList());
_totalSize = productList.totalSize;
}

void clear() {
_products.clear();
_totalSize = 0;
}
}
75 changes: 40 additions & 35 deletions packages/smooth_app/lib/data_models/product_list.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/utils/PnnsGroups.dart';

enum ProductListType {
/// API search for [PnnsGroup2Filter] related food groups
HTTP_SEARCH_GROUP,

/// API search by [SearchTerms] keywords
HTTP_SEARCH_KEYWORDS,

Expand All @@ -20,7 +16,6 @@ enum ProductListType {

extension ProductListTypeExtension on ProductListType {
static const Map<ProductListType, String> _keys = <ProductListType, String>{
ProductListType.HTTP_SEARCH_GROUP: 'http/search/group',
ProductListType.HTTP_SEARCH_KEYWORDS: 'http/search/keywords',
ProductListType.HTTP_SEARCH_CATEGORY: 'http/search/category',
ProductListType.SCAN_SESSION: 'scan_session',
Expand All @@ -31,24 +26,33 @@ extension ProductListTypeExtension on ProductListType {
}

class ProductList {
ProductList._({required this.listType, this.parameters = ''});

ProductList.keywordSearch(final String keywords)
: this._(
ProductList._({
required this.listType,
this.parameters = '',
this.pageSize = 0,
this.pageNumber = 0,
});

ProductList.keywordSearch(
final String keywords, {
required int pageSize,
required int pageNumber,
}) : this._(
listType: ProductListType.HTTP_SEARCH_KEYWORDS,
parameters: keywords,
pageSize: pageSize,
pageNumber: pageNumber,
);

ProductList.categorySearch(final String category)
: this._(
ProductList.categorySearch(
final String category, {
required int pageSize,
required int pageNumber,
}) : this._(
listType: ProductListType.HTTP_SEARCH_CATEGORY,
parameters: category,
);

ProductList.groupSearch(final PnnsGroup2 group)
: this._(
listType: ProductListType.HTTP_SEARCH_GROUP,
parameters: group.id,
pageSize: pageSize,
pageNumber: pageNumber,
);

ProductList.history() : this._(listType: ProductListType.HISTORY);
Expand All @@ -58,33 +62,24 @@ class ProductList {
final ProductListType listType;
final String parameters;

final List<String> _barcodes = <String>[];
final Map<String, Product> _products = <String, Product>{};

/// API search for [PnnsGroup2Filter] related food groups
static const String LIST_TYPE_HTTP_SEARCH_GROUP = 'http/search/group';

/// API search by [SearchTerms] keywords
static const String LIST_TYPE_HTTP_SEARCH_KEYWORDS = 'http/search/keywords';
/// Page size at query time.
final int? pageSize;

/// API search for [CategoryProductQuery] category
static const String LIST_TYPE_HTTP_SEARCH_CATEGORY = 'http/search/category';
/// Page number at query time.
final int? pageNumber;

/// Current scan session; can be easily cleared by the end-user
static const String LIST_TYPE_SCAN_SESSION = 'scan_session';
/// "Total size" returned by the query.
int totalSize = 0;

/// History of products seen by the end-user
static const String LIST_TYPE_HISTORY = 'history';
final List<String> _barcodes = <String>[];
final Map<String, Product> _products = <String, Product>{};

List<String> get barcodes => _barcodes;

bool isEmpty() => _barcodes.isEmpty;

Product getProduct(final String barcode) => _products[barcode]!;

bool isSameAs(final ProductList other) =>
listType == other.listType && parameters == other.parameters;

void refresh(final Product product) {
final String? barcode = product.barcode;
if (barcode == null) {
Expand Down Expand Up @@ -144,7 +139,6 @@ class ProductList {

bool _isReversed() {
switch (listType) {
case ProductListType.HTTP_SEARCH_GROUP:
case ProductListType.HTTP_SEARCH_KEYWORDS:
case ProductListType.HTTP_SEARCH_CATEGORY:
return false;
Expand All @@ -153,4 +147,15 @@ class ProductList {
return true;
}
}

String getParametersKey() {
switch (listType) {
case ProductListType.SCAN_SESSION:
case ProductListType.HISTORY:
return parameters;
case ProductListType.HTTP_SEARCH_KEYWORDS:
case ProductListType.HTTP_SEARCH_CATEGORY:
return '$parameters,$pageSize,$pageNumber';
}
}
}
26 changes: 18 additions & 8 deletions packages/smooth_app/lib/data_models/product_list_supplier.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/data_models/database_product_list_supplier.dart';
import 'package:smooth_app/data_models/partial_product_list.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/data_models/query_product_list_supplier.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/database/product_query.dart';
import 'package:smooth_app/database/paged_product_query.dart';

/// Asynchronously loads a [ProductList] with products
abstract class ProductListSupplier {
Expand All @@ -14,23 +14,33 @@ abstract class ProductListSupplier {
this.timestamp,
});

final ProductQuery productQuery;
final PagedProductQuery productQuery;
final LocalDatabase localDatabase;
final int? timestamp;
@protected
late ProductList productList;
final PartialProductList partialProductList = PartialProductList();

/// Returns null if OK, or the message error
Future<String?> asyncLoad();

ProductList getProductList() => productList;

/// Returns a helper supplier in order to refresh the data
ProductListSupplier? getRefreshSupplier();

/// Clears the database and restarts from top page.
Future<void> clear() async {
final DaoProductList daoProductList = DaoProductList(localDatabase);
productQuery.toTopPage();
while (await daoProductList.delete(productQuery.getProductList())) {
productQuery.toNextPage();
}

productQuery.toTopPage();

partialProductList.clear();
}

/// Returns the fastest data supplier: database if possible, or server query
static Future<ProductListSupplier> getBestSupplier(
final ProductQuery productQuery,
final PagedProductQuery productQuery,
final LocalDatabase localDatabase,
) async {
final int? timestamp = await DaoProductList(localDatabase).getTimestamp(
Expand Down
Loading

0 comments on commit e9ec9b3

Please sign in to comment.