forked from openfoodfacts/smooth-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: background tasks with classes (openfoodfacts#2994)
New files: * `abstract_background_task.dart`: Abstract background task. * `background_task_details.dart`: Background task that changes product details (data, but no image upload). * `background_task_image.dart`: Background task about product image upload. Impacted files: * `add_basic_details_page.dart`: refactored the call to background task; minor refactoring * `background_task_helper.dart`: moved most of the code to new classes `AbstractBackgroundTask` and offsprings. * `edit_ingredients_page.dart`: refactored the call to background task * `nutrition_page_loaded.dart`: refactored the call to background task * `picture_capture_helper.dart`: refactored the call to background task * `simple_input_page.dart`: refactored the call to background task * `simple_input_page_helpers.dart`: added and implemented method `getTask`
- Loading branch information
1 parent
d2f8077
commit 68b6939
Showing
10 changed files
with
455 additions
and
392 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
packages/smooth_app/lib/background/abstract_background_task.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:openfoodfacts/openfoodfacts.dart'; | ||
import 'package:openfoodfacts/utils/CountryHelper.dart'; | ||
import 'package:smooth_app/background/background_task_details.dart'; | ||
import 'package:smooth_app/background/background_task_image.dart'; | ||
import 'package:smooth_app/database/local_database.dart'; | ||
import 'package:smooth_app/query/product_query.dart'; | ||
import 'package:task_manager/task_manager.dart'; | ||
|
||
/// Abstract background task. | ||
abstract class AbstractBackgroundTask { | ||
const AbstractBackgroundTask({ | ||
required this.processName, | ||
required this.uniqueId, | ||
required this.barcode, | ||
required this.languageCode, | ||
required this.user, | ||
required this.country, | ||
}); | ||
|
||
/// Typically, similar to the name of the class that extends this one. | ||
/// | ||
/// To be used when deserializing, in order to check who is who. | ||
final String processName; | ||
|
||
/// Unique task identifier, needed e.g. for task overwriting. | ||
final String uniqueId; | ||
|
||
final String barcode; | ||
final String languageCode; | ||
final String user; | ||
final String country; | ||
|
||
@protected | ||
Map<String, dynamic> toJson(); | ||
|
||
/// Returns the deserialized background task if possible, or null. | ||
static AbstractBackgroundTask? fromTask(final Task task) => | ||
BackgroundTaskDetails.fromTask(task) ?? | ||
BackgroundTaskImage.fromTask(task); | ||
|
||
/// Response code sent by the server in case of a success. | ||
@protected | ||
static const int SUCCESS_CODE = 1; | ||
|
||
/// Executes the background task. | ||
Future<TaskResult> execute(final LocalDatabase localDatabase); | ||
|
||
@protected | ||
OpenFoodFactsLanguage getLanguage() => LanguageHelper.fromJson(languageCode); | ||
|
||
@protected | ||
OpenFoodFactsCountry? getCountry() => CountryHelper.fromJson(country); | ||
|
||
/// Generates a unique id for the background task. | ||
/// | ||
/// This ensures that the background task is unique and also | ||
/// ensures that in case of conflicts, the background task is replaced. | ||
/// Example: 8901072002478_B_en_in_username | ||
@protected | ||
static String generateUniqueId( | ||
String barcode, | ||
String processIdentifier, { | ||
final bool appendTimestamp = false, | ||
}) { | ||
final StringBuffer stringBuffer = StringBuffer(); | ||
stringBuffer | ||
..write(barcode) | ||
..write('_') | ||
..write(processIdentifier) | ||
..write('_') | ||
..write(ProductQuery.getLanguage().code) | ||
..write('_') | ||
..write(ProductQuery.getCountry()!.iso2Code) | ||
..write('_') | ||
..write(ProductQuery.getUser().userId); | ||
if (appendTimestamp) { | ||
stringBuffer | ||
..write('_') | ||
..write(DateTime.now().millisecondsSinceEpoch); | ||
} | ||
return stringBuffer.toString(); | ||
} | ||
} |
171 changes: 171 additions & 0 deletions
171
packages/smooth_app/lib/background/background_task_details.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:openfoodfacts/openfoodfacts.dart'; | ||
import 'package:openfoodfacts/utils/CountryHelper.dart'; | ||
import 'package:smooth_app/background/abstract_background_task.dart'; | ||
import 'package:smooth_app/database/dao_product.dart'; | ||
import 'package:smooth_app/database/local_database.dart'; | ||
import 'package:smooth_app/query/product_query.dart'; | ||
import 'package:task_manager/task_manager.dart'; | ||
|
||
/// Background task that changes product details (data, but no image upload). | ||
class BackgroundTaskDetails extends AbstractBackgroundTask { | ||
const BackgroundTaskDetails._({ | ||
required super.processName, | ||
required super.uniqueId, | ||
required super.barcode, | ||
required super.languageCode, | ||
required super.user, | ||
required super.country, | ||
required this.inputMap, | ||
}); | ||
|
||
BackgroundTaskDetails._fromJson(Map<String, dynamic> json) | ||
: this._( | ||
processName: json['processName'] as String, | ||
uniqueId: json['uniqueId'] as String, | ||
barcode: json['barcode'] as String, | ||
languageCode: json['languageCode'] as String, | ||
user: json['user'] as String, | ||
country: json['country'] as String, | ||
inputMap: json['inputMap'] as String, | ||
); | ||
|
||
/// Task ID. | ||
static const String _PROCESS_NAME = 'PRODUCT_EDIT'; | ||
|
||
/// Serialized product. | ||
final String inputMap; | ||
|
||
@override | ||
Map<String, dynamic> toJson() => <String, dynamic>{ | ||
'processName': processName, | ||
'uniqueId': uniqueId, | ||
'barcode': barcode, | ||
'languageCode': languageCode, | ||
'user': user, | ||
'country': country, | ||
'inputMap': inputMap, | ||
}; | ||
|
||
/// Returns the deserialized background task if possible, or null. | ||
static AbstractBackgroundTask? fromTask(final Task task) { | ||
try { | ||
final AbstractBackgroundTask result = | ||
BackgroundTaskDetails._fromJson(task.data!); | ||
if (result.processName == _PROCESS_NAME) { | ||
return result; | ||
} | ||
} catch (e) { | ||
// | ||
} | ||
return null; | ||
} | ||
|
||
/// Adds the background task about changing a product. | ||
/// | ||
/// Either [productEditTask] or [productEditTasks] must be populated; | ||
/// we need that for classification purpose (and unique id computation). | ||
static Future<void> addTask( | ||
final Product minimalistProduct, { | ||
final List<ProductEditTask>? productEditTasks, | ||
final ProductEditTask? productEditTask, | ||
}) async { | ||
final String code; | ||
if (productEditTask != null) { | ||
if (productEditTasks != null) { | ||
throw Exception(); | ||
} | ||
code = productEditTask.code; | ||
} else { | ||
if (productEditTasks == null || productEditTasks.isEmpty) { | ||
throw Exception(); | ||
} | ||
final StringBuffer buffer = StringBuffer(); | ||
for (final ProductEditTask task in productEditTasks) { | ||
buffer.write(task.code); | ||
} | ||
code = buffer.toString(); | ||
} | ||
final String uniqueId = AbstractBackgroundTask.generateUniqueId( | ||
minimalistProduct.barcode!, | ||
code, | ||
); | ||
final BackgroundTaskDetails backgroundTask = BackgroundTaskDetails._( | ||
uniqueId: uniqueId, | ||
processName: _PROCESS_NAME, | ||
barcode: minimalistProduct.barcode!, | ||
languageCode: ProductQuery.getLanguage().code, | ||
inputMap: jsonEncode(minimalistProduct.toJson()), | ||
user: jsonEncode(ProductQuery.getUser().toJson()), | ||
country: ProductQuery.getCountry()!.iso2Code, | ||
); | ||
await TaskManager().addTask( | ||
Task( | ||
data: backgroundTask.toJson(), | ||
uniqueId: uniqueId, | ||
), | ||
); | ||
} | ||
|
||
/// Uploads the product changes, downloads the whole product, updates locally. | ||
@override | ||
Future<TaskResult> execute(final LocalDatabase localDatabase) async { | ||
final Map<String, dynamic> productMap = | ||
json.decode(inputMap) as Map<String, dynamic>; | ||
final User user = | ||
User.fromJson(jsonDecode(this.user) as Map<String, dynamic>); | ||
|
||
await OpenFoodAPIClient.saveProduct( | ||
user, | ||
Product.fromJson(productMap), | ||
language: getLanguage(), | ||
country: getCountry(), | ||
); | ||
|
||
final DaoProduct daoProduct = DaoProduct(localDatabase); | ||
final ProductQueryConfiguration configuration = ProductQueryConfiguration( | ||
barcode, | ||
fields: ProductQuery.fields, | ||
language: getLanguage(), | ||
country: getCountry(), | ||
); | ||
|
||
final ProductResult queryResult = | ||
await OpenFoodAPIClient.getProduct(configuration); | ||
|
||
if (queryResult.status == AbstractBackgroundTask.SUCCESS_CODE) { | ||
final Product? product = queryResult.product; | ||
if (product != null) { | ||
await daoProduct.put(product); | ||
localDatabase.notifyListeners(); | ||
} | ||
} | ||
|
||
// Returns true to let platform know that the task is completed | ||
return TaskResult.success; | ||
} | ||
} | ||
|
||
/// Product edit single tasks. | ||
/// | ||
/// Used for classification (and unique id computation). | ||
enum ProductEditTask { | ||
nutrition('N'), | ||
packaging('P'), | ||
ingredient('I'), | ||
basic('B'), | ||
store('S'), | ||
origin('O'), | ||
emb('E'), | ||
label('L'), | ||
category('K'), | ||
country('C'); | ||
|
||
const ProductEditTask(this.code); | ||
|
||
/// Code used to distinguish the tasks. | ||
/// | ||
/// Of course there shouldn't be duplicates. | ||
final String code; | ||
} |
137 changes: 137 additions & 0 deletions
137
packages/smooth_app/lib/background/background_task_image.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
import 'package:openfoodfacts/openfoodfacts.dart'; | ||
import 'package:openfoodfacts/utils/CountryHelper.dart'; | ||
import 'package:smooth_app/background/abstract_background_task.dart'; | ||
import 'package:smooth_app/database/dao_product.dart'; | ||
import 'package:smooth_app/database/local_database.dart'; | ||
import 'package:smooth_app/query/product_query.dart'; | ||
import 'package:task_manager/task_manager.dart'; | ||
|
||
/// Background task about product image upload. | ||
class BackgroundTaskImage extends AbstractBackgroundTask { | ||
const BackgroundTaskImage._({ | ||
required super.processName, | ||
required super.uniqueId, | ||
required super.barcode, | ||
required super.languageCode, | ||
required super.user, | ||
required super.country, | ||
required this.imageField, | ||
required this.imagePath, | ||
}); | ||
|
||
BackgroundTaskImage._fromJson(Map<String, dynamic> json) | ||
: this._( | ||
processName: json['processName'] as String, | ||
uniqueId: json['uniqueId'] as String, | ||
barcode: json['barcode'] as String, | ||
languageCode: json['languageCode'] as String, | ||
user: json['user'] as String, | ||
country: json['country'] as String, | ||
imageField: json['imageField'] as String, | ||
imagePath: json['imagePath'] as String, | ||
); | ||
|
||
/// Task ID. | ||
static const String _PROCESS_NAME = 'IMAGE_UPLOAD'; | ||
|
||
final String imageField; | ||
final String imagePath; | ||
|
||
@override | ||
Map<String, dynamic> toJson() => <String, dynamic>{ | ||
'processName': processName, | ||
'uniqueId': uniqueId, | ||
'barcode': barcode, | ||
'languageCode': languageCode, | ||
'user': user, | ||
'country': country, | ||
'imageField': imageField, | ||
'imagePath': imagePath, | ||
}; | ||
|
||
/// Returns the deserialized background task if possible, or null. | ||
static AbstractBackgroundTask? fromTask(final Task task) { | ||
try { | ||
final AbstractBackgroundTask result = | ||
BackgroundTaskImage._fromJson(task.data!); | ||
if (result.processName == _PROCESS_NAME) { | ||
return result; | ||
} | ||
} catch (e) { | ||
// | ||
} | ||
return null; | ||
} | ||
|
||
/// Adds the background task about uploading a product image. | ||
static Future<void> addTask( | ||
final String barcode, { | ||
required final ImageField imageField, | ||
required final File imageFile, | ||
}) async { | ||
// For "OTHER" images we randomize the id with timestamp | ||
// so that it runs separately. | ||
final String uniqueId = AbstractBackgroundTask.generateUniqueId( | ||
barcode, | ||
imageField.value, | ||
appendTimestamp: imageField == ImageField.OTHER, | ||
); | ||
final BackgroundTaskImage backgroundImageInputData = BackgroundTaskImage._( | ||
uniqueId: uniqueId, | ||
barcode: barcode, | ||
processName: _PROCESS_NAME, | ||
imageField: imageField.value, | ||
imagePath: imageFile.path, | ||
languageCode: ProductQuery.getLanguage().code, | ||
user: jsonEncode(ProductQuery.getUser().toJson()), | ||
country: ProductQuery.getCountry()!.iso2Code, | ||
); | ||
await TaskManager().addTask( | ||
Task( | ||
data: backgroundImageInputData.toJson(), | ||
uniqueId: uniqueId, | ||
), | ||
); | ||
} | ||
|
||
/// Uploads the product image, downloads the whole product, updates locally. | ||
@override | ||
Future<TaskResult> execute(final LocalDatabase localDatabase) async { | ||
final User user = | ||
User.fromJson(jsonDecode(this.user) as Map<String, dynamic>); | ||
|
||
final SendImage image = SendImage( | ||
lang: getLanguage(), | ||
barcode: barcode, | ||
imageField: ImageFieldExtension.getType(imageField), | ||
imageUri: Uri.parse(imagePath), | ||
); | ||
|
||
await OpenFoodAPIClient.addProductImage(user, image); | ||
|
||
// Go to the file system and delete the file that was uploaded | ||
File(imagePath).deleteSync(); | ||
final DaoProduct daoProduct = DaoProduct(localDatabase); | ||
final ProductQueryConfiguration configuration = ProductQueryConfiguration( | ||
barcode, | ||
fields: ProductQuery.fields, | ||
language: getLanguage(), | ||
country: getCountry(), | ||
); | ||
|
||
final ProductResult queryResult = | ||
await OpenFoodAPIClient.getProduct(configuration); | ||
if (queryResult.status == AbstractBackgroundTask.SUCCESS_CODE) { | ||
final Product? product = queryResult.product; | ||
if (product != null) { | ||
await daoProduct.put(product); | ||
localDatabase.notifyListeners(); | ||
} | ||
} | ||
|
||
return TaskResult.success; | ||
} | ||
} |
Oops, something went wrong.