Skip to content

Commit

Permalink
feature/#21 - automatically storing keywords and group results into t…
Browse files Browse the repository at this point in the history
…he database

... and automagically asking the end-user if they want to see the cache

New files:
* `dao_product.dart`
* `dao_product_list.dart`
* `database_product_list_supplier.dart`
* `product_list.dart`
* `product_list_supplier.dart`
* `product_query_page_helper.dart`
* `query_product_list_supplier.dart`

Impacted files:
* `choose_page.dart`: now uses `ProductQueryPageHelper` in order to get database or http results; refactored
* `continuous_scan_model.dart`: minor refactoring due to DAO
* `group_product_query.dart`: now implements new method `getProductList`
* `keywords_product_query.dart`: now implements new method `getProductList`
* `local_database.dart`: now it's version 2; refactored using the new DAO classes
* `product_query.dart`: new method `getProductList`
* `product_query_model.dart`: now we use a `ProductListSupplier` instead of a `ProductQuery`
* `product_query_page.dart`: now we use a `ProductListSupplier` instead of a `ProductQuery`
  • Loading branch information
monsieurtanuki committed Jan 10, 2021
1 parent 5faab97 commit 43fda6e
Show file tree
Hide file tree
Showing 15 changed files with 688 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:smooth_app/database/barcode_product_query.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/local_database.dart';

enum ScannedProductState {
Expand Down Expand Up @@ -96,7 +97,7 @@ class ContinuousScanModel {
Future<void> _loadBarcode(final String barcode) async {
final Product product = await BarcodeProductQuery(barcode).getProduct();
if (product != null) {
_localDatabase.putProduct(product);
await DaoProduct(_localDatabase).put(product);
_products[barcode] = product;
setBarcodeState(barcode, ScannedProductState.FOUND);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:smooth_app/data_models/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/data_models/product_list.dart';
import 'package:smooth_app/database/product_query.dart';

class DatabaseProductListSupplier implements ProductListSupplier {
DatabaseProductListSupplier(this.productQuery, this.localDatabase);

final ProductQuery productQuery;
final LocalDatabase localDatabase;
ProductList _productList;

@override
ProductList getProductList() => _productList;

@override
Future<String> asyncLoad() async {
try {
final ProductList productList = productQuery.getProductList();
final bool result = await DaoProductList(localDatabase).get(productList);
if (!result) {
return 'unexpected empty record';
}
_productList = productList;
return null;
} catch (e) {
return e.toString();
}
}

@override
bool needsToBeSavedIntoDb() => false;
}
62 changes: 62 additions & 0 deletions packages/smooth_app/lib/data_models/product_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:openfoodfacts/model/Product.dart';

class ProductList {
ProductList({
@required this.listType,
@required this.parameters,
});

final String listType;
final String parameters;

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

static const String LIST_TYPE_HTTP_SEARCH_GROUP = 'http/search/group';
static const String LIST_TYPE_HTTP_SEARCH_KEYWORDS = 'http/search/keywords';

List<String> get barcodes => _barcodes;

bool isEmpty() => _barcodes.isEmpty;

void clear() {
_barcodes.clear();
_products.clear();
}

void add(final Product product) {
if (product == null) {
throw Exception('null product');
}
final String barcode = product.barcode;
if (barcode == null) {
throw Exception('null barcode');
}
_barcodes.add(barcode);
_products[barcode] = product;
}

void addAll(final List<Product> products) => products.forEach(add);

void set(
final List<String> barcodes,
final Map<String, Product> products,
) {
clear();
_barcodes.addAll(barcodes);
_products.addAll(products);
}

List<Product> getList() {
final List<Product> result = <Product>[];
for (final String barcode in _barcodes) {
final Product product = _products[barcode];
if (product == null) {
throw Exception('no product for barcode $barcode');
}
result.add(product);
}
return result;
}
}
10 changes: 10 additions & 0 deletions packages/smooth_app/lib/data_models/product_list_supplier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:smooth_app/data_models/product_list.dart';

abstract class ProductListSupplier {
/// returns null if OK, or the message error
Future<String> asyncLoad();

ProductList getProductList();

bool needsToBeSavedIntoDb();
}
30 changes: 18 additions & 12 deletions packages/smooth_app/lib/data_models/product_query_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import 'package:openfoodfacts/model/Product.dart';
import 'package:smooth_app/data_models/user_preferences_model.dart';
import 'package:smooth_app/data_models/match.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:openfoodfacts/model/SearchResult.dart';
import 'package:smooth_app/database/product_query.dart';
import 'package:smooth_app/temp/user_preferences.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/data_models/product_list_supplier.dart';

enum LoadingStatus {
LOADING,
Expand All @@ -16,10 +17,12 @@ enum LoadingStatus {
}

class ProductQueryModel with ChangeNotifier {
ProductQueryModel(final ProductQuery productQuery) {
_asyncLoad(productQuery);
ProductQueryModel(this.supplier) {
_asyncLoad();
}

final ProductListSupplier supplier;

static const String _CATEGORY_ALL = 'all';

LoadingStatus _loadingStatus = LoadingStatus.LOADING;
Expand All @@ -35,14 +38,13 @@ class ProductQueryModel with ChangeNotifier {
String get loadingError => _loadingError;
LoadingStatus get loadingStatus => _loadingStatus;

Future<void> _asyncLoad(final ProductQuery productQuery) async {
try {
final SearchResult searchResult = await productQuery.getSearchResult();
_products = searchResult.products;
_loadingStatus = LoadingStatus.LOADED;
} catch (e) {
Future<void> _asyncLoad() async {
_loadingError = await supplier.asyncLoad();
if (_loadingError != null) {
_loadingStatus = LoadingStatus.ERROR;
_loadingError = e.toString();
} else {
_loadingStatus = LoadingStatus.LOADED;
_products = supplier.getProductList().getList();
}
notifyListeners();
}
Expand All @@ -57,7 +59,11 @@ class ProductQueryModel with ChangeNotifier {
}
_loadingStatus = LoadingStatus.POST_LOAD_STARTED;

localDatabase.putProducts(_products);
final ProductList productList = supplier.getProductList();
_products = productList.getList();
if (supplier.needsToBeSavedIntoDb()) {
DaoProductList(localDatabase).put(productList);
}
Match.sort(_products, userPreferences, userPreferencesModel);

displayProducts = _products;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:smooth_app/data_models/product_list_supplier.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/database/product_query.dart';
import 'package:openfoodfacts/model/SearchResult.dart';

class QueryProductListSupplier implements ProductListSupplier {
QueryProductListSupplier(this.productQuery);

final ProductQuery productQuery;
ProductList _productList;

@override
ProductList getProductList() => _productList;

@override
Future<String> asyncLoad() async {
try {
final SearchResult searchResult = await productQuery.getSearchResult();
_productList = productQuery.getProductList();
_productList.addAll(searchResult.products);
return null;
} catch (e) {
return e.toString();
}
}

@override
bool needsToBeSavedIntoDb() => true;
}
112 changes: 112 additions & 0 deletions packages/smooth_app/lib/database/dao_product.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'dart:async';
import 'dart:convert';
import 'package:smooth_app/database/local_database.dart';
import 'package:sqflite/sqflite.dart';
import 'package:openfoodfacts/model/Product.dart';

class DaoProduct {
DaoProduct(this.localDatabase);

final LocalDatabase localDatabase;

static const String TABLE_PRODUCT = 'product';
static const String TABLE_PRODUCT_COLUMN_BARCODE = 'barcode';
static const String _TABLE_PRODUCT_COLUMN_JSON = 'encoded_json';

static FutureOr<void> onUpgrade(
final Database db,
final int oldVersion,
final int newVersion,
) async {
if (oldVersion < 1) {
await db.execute('create table $TABLE_PRODUCT('
'$TABLE_PRODUCT_COLUMN_BARCODE TEXT PRIMARY KEY,'
'$_TABLE_PRODUCT_COLUMN_JSON TEXT NOT NULL,'
'${LocalDatabase.COLUMN_TIMESTAMP} INT NOT NULL'
')');
}
}

Future<Product> get(final String barcode) async {
final List<Map<String, dynamic>> queryResult =
await localDatabase.database.query(
TABLE_PRODUCT,
columns: <String>[_TABLE_PRODUCT_COLUMN_JSON],
where: '$TABLE_PRODUCT_COLUMN_BARCODE = ?',
whereArgs: <String>[barcode],
);
if (queryResult.isEmpty) {
// not found
return null;
}
if (queryResult.length > 1) {
// very very unlikely to happen
throw Exception('Several products with the same barcode $barcode');
}
return _getProductFromQueryResult(queryResult[0]);
}

Future<Map<String, Product>> getAll(final List<String> barcodes) async {
final Map<String, Product> result = <String, Product>{};
if (barcodes == null || barcodes.isEmpty) {
return result;
}
final List<Map<String, dynamic>> queryResults =
await localDatabase.database.query(
TABLE_PRODUCT,
columns: <String>[
TABLE_PRODUCT_COLUMN_BARCODE,
_TABLE_PRODUCT_COLUMN_JSON,
],
where:
'$TABLE_PRODUCT_COLUMN_BARCODE in(? ${',?' * (barcodes.length - 1)})',
whereArgs: barcodes,
);
if (queryResults.isEmpty) {
return result;
}
for (final Map<String, dynamic> row in queryResults) {
result[row[TABLE_PRODUCT_COLUMN_BARCODE] as String] =
_getProductFromQueryResult(row);
}
return result;
}

Future<void> put(final Product product) async =>
await _put(product, localDatabase.database);

Future<void> putProducts(final List<Product> products) async =>
await localDatabase.database
.transaction((final Transaction transaction) async {
for (final Product product in products) {
await _put(product, transaction);
}
});

static Future<void> _put(
final Product product,
final DatabaseExecutor databaseExecutor,
) async {
await databaseExecutor.execute(
'insert into $TABLE_PRODUCT('
' $TABLE_PRODUCT_COLUMN_BARCODE,'
' $_TABLE_PRODUCT_COLUMN_JSON,'
' ${LocalDatabase.COLUMN_TIMESTAMP}'
')values(?, ?, ?)'
' on conflict($TABLE_PRODUCT_COLUMN_BARCODE) DO UPDATE SET '
' $_TABLE_PRODUCT_COLUMN_JSON=excluded.$_TABLE_PRODUCT_COLUMN_JSON,'
' ${LocalDatabase.COLUMN_TIMESTAMP}=excluded.${LocalDatabase.COLUMN_TIMESTAMP}',
<dynamic>[
product.barcode,
json.encode(product.toJson()),
LocalDatabase.nowInMillis(),
]); // TODO(monsieurtanuki): check if this upsert does not cause delete+insert, but just update
}

Product _getProductFromQueryResult(final Map<String, dynamic> row) {
final String encodedJson = row[_TABLE_PRODUCT_COLUMN_JSON] as String;
final Map<String, dynamic> decodedJson =
json.decode(encodedJson) as Map<String, dynamic>;
return Product.fromJson(decodedJson);
}
}
Loading

0 comments on commit 43fda6e

Please sign in to comment.