Skip to content

Commit 75fc7b5

Browse files
fix: 6249 - refresh of the prices of a product after a related price addition (#6361)
New file: * `product_price_refresher.dart`: Async refresh of the prices of a product, with several loading phases. Impacted files: * `background_task_add_other_price.dart`: minor refactoring * `background_task_add_price.dart`: minor refactoring * `background_task_price.dart`: now aftrer we add a price, we signal that the related product price counts needs to be refreshed * `prices_card.dart`: used the new `ProductPriceRefresher` class for async data loading * `product_prices_list.dart`: used the new `ProductPriceRefresher` class for async data loading; refactored the display of the list in a separate class
1 parent dff1c58 commit 75fc7b5

File tree

6 files changed

+292
-177
lines changed

6 files changed

+292
-177
lines changed

packages/smooth_app/lib/background/background_task_add_other_price.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class BackgroundTaskAddOtherPrice extends BackgroundTaskPrice {
121121
await addPrices(
122122
bearerToken: bearerToken,
123123
proofId: proofId,
124+
localDatabase: localDatabase,
124125
);
125126

126127
await closeSession(bearerToken: bearerToken);

packages/smooth_app/lib/background/background_task_add_price.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class BackgroundTaskAddPrice extends BackgroundTaskPrice {
256256
await addPrices(
257257
bearerToken: bearerToken,
258258
proofId: uploadProof.value.id,
259+
localDatabase: localDatabase,
259260
);
260261

261262
await closeSession(bearerToken: bearerToken);

packages/smooth_app/lib/background/background_task_price.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
55
import 'package:openfoodfacts/openfoodfacts.dart';
66
import 'package:smooth_app/background/background_task.dart';
77
import 'package:smooth_app/database/local_database.dart';
8+
import 'package:smooth_app/pages/prices/product_price_refresher.dart';
89
import 'package:smooth_app/query/product_query.dart';
910

1011
/// Abstract background task about adding prices.
@@ -174,8 +175,10 @@ abstract class BackgroundTaskPrice extends BackgroundTask {
174175
Future<void> addPrices({
175176
required final String bearerToken,
176177
required final int proofId,
178+
required final LocalDatabase localDatabase,
177179
}) async {
178180
for (int i = 0; i < barcodes.length; i++) {
181+
final String barcode = barcodes[i];
179182
final Price newPrice = Price()
180183
..date = date
181184
..currency = currency
@@ -185,7 +188,7 @@ abstract class BackgroundTaskPrice extends BackgroundTask {
185188
..priceIsDiscounted = pricesAreDiscounted[i]
186189
..price = prices[i]
187190
..priceWithoutDiscount = pricesWithoutDiscount[i]
188-
..productCode = barcodes[i];
191+
..productCode = barcode;
189192

190193
// create price
191194
final MaybeError<Price?> addedPrice =
@@ -197,7 +200,9 @@ abstract class BackgroundTaskPrice extends BackgroundTask {
197200
if (addedPrice.isError) {
198201
throw Exception('Could not add price: ${addedPrice.error}');
199202
}
203+
ProductPriceRefresher.setLatestUpdate(barcode);
200204
}
205+
localDatabase.notifyListeners();
201206
}
202207

203208
@protected

packages/smooth_app/lib/pages/prices/prices_card.dart

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1+
import 'dart:async';
12
import 'dart:math';
23

34
import 'package:flutter/cupertino.dart';
45
import 'package:flutter/material.dart';
56
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
67
import 'package:openfoodfacts/openfoodfacts.dart';
8+
import 'package:provider/provider.dart';
9+
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
10+
import 'package:smooth_app/database/local_database.dart';
711
import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart';
812
import 'package:smooth_app/generic_lib/design_constants.dart';
913
import 'package:smooth_app/helpers/product_cards_helper.dart';
1014
import 'package:smooth_app/pages/prices/get_prices_model.dart';
1115
import 'package:smooth_app/pages/prices/price_meta_product.dart';
1216
import 'package:smooth_app/pages/prices/prices_page.dart';
1317
import 'package:smooth_app/pages/prices/product_price_add_page.dart';
14-
import 'package:smooth_app/query/product_query.dart';
18+
import 'package:smooth_app/pages/prices/product_price_refresher.dart';
1519
import 'package:smooth_app/resources/app_icons.dart' as icons;
1620
import 'package:smooth_app/themes/smooth_theme.dart';
1721
import 'package:smooth_app/themes/smooth_theme_colors.dart';
@@ -130,67 +134,64 @@ class _PricesCardViewButton extends StatefulWidget {
130134
}
131135

132136
class _PricesCardViewButtonState extends State<_PricesCardViewButton> {
133-
late final GetPricesModel _model;
134-
late final Future<MaybeError<GetPricesResult>> _prices = _showProductPrices();
137+
GetPricesModel? _model;
138+
ProductPriceRefresher? _productPriceRefresher;
135139

136140
@override
137-
Widget build(BuildContext context) =>
138-
FutureBuilder<MaybeError<GetPricesResult>>(
139-
future: _prices,
140-
builder: (
141-
final BuildContext context,
142-
final AsyncSnapshot<MaybeError<GetPricesResult>> snapshot,
143-
) {
144-
final AppLocalizations appLocalizations =
145-
AppLocalizations.of(context);
146-
GetPricesResult? pricesResult;
147-
if (snapshot.hasData && !snapshot.data!.isError) {
148-
pricesResult = snapshot.data!.value;
149-
}
150-
return Badge(
151-
offset: Offset.zero,
152-
isLabelVisible: pricesResult?.total != null,
153-
backgroundColor:
154-
context.extension<SmoothColorsThemeExtension>().secondaryNormal,
155-
label: Padding(
156-
padding: const EdgeInsetsDirectional.only(
157-
start: VERY_SMALL_SPACE,
158-
end: VERY_SMALL_SPACE,
159-
top: VERY_SMALL_SPACE,
160-
bottom: 6.0,
161-
),
162-
child: Text(
163-
'${pricesResult?.total}',
164-
style: const TextStyle(
165-
color: Colors.white,
166-
fontWeight: FontWeight.bold,
167-
),
168-
),
169-
),
170-
child: SmoothLargeButtonWithIcon(
171-
text: appLocalizations.prices_view_prices,
172-
leadingIcon: const Icon(CupertinoIcons.tag_fill),
173-
onPressed: () async => Navigator.of(context).push(
174-
MaterialPageRoute<void>(
175-
builder: (BuildContext context) => PricesPage(
176-
_model,
177-
pricesResult: pricesResult,
178-
),
179-
),
180-
),
181-
),
182-
);
183-
},
184-
);
141+
Widget build(BuildContext context) {
142+
final AppLocalizations appLocalizations = AppLocalizations.of(context);
185143

186-
Future<MaybeError<GetPricesResult>> _showProductPrices() async {
187-
_model = GetPricesModel.product(
144+
_model ??= GetPricesModel.product(
188145
product: PriceMetaProduct.product(widget.product),
189146
context: context,
190147
);
191-
return OpenPricesAPIClient.getPrices(
192-
_model.parameters,
193-
uriHelper: ProductQuery.uriPricesHelper,
148+
_productPriceRefresher ??= ProductPriceRefresher(
149+
model: _model!,
150+
userPreferences: context.read<UserPreferences>(),
151+
pricesResult: null,
152+
refreshDisplay: () {
153+
if (mounted) {
154+
setState(() {});
155+
}
156+
},
157+
);
158+
159+
context.watch<LocalDatabase>();
160+
unawaited(_productPriceRefresher!.runIfNeeded());
161+
162+
final int? total = _productPriceRefresher!.pricesResult?.total;
163+
return Badge(
164+
offset: Offset.zero,
165+
isLabelVisible: total != null,
166+
backgroundColor:
167+
context.extension<SmoothColorsThemeExtension>().secondaryNormal,
168+
label: Padding(
169+
padding: const EdgeInsetsDirectional.only(
170+
start: VERY_SMALL_SPACE,
171+
end: VERY_SMALL_SPACE,
172+
top: VERY_SMALL_SPACE,
173+
bottom: 6.0,
174+
),
175+
child: Text(
176+
'$total',
177+
style: const TextStyle(
178+
color: Colors.white,
179+
fontWeight: FontWeight.bold,
180+
),
181+
),
182+
),
183+
child: SmoothLargeButtonWithIcon(
184+
text: appLocalizations.prices_view_prices,
185+
leadingIcon: const Icon(CupertinoIcons.tag_fill),
186+
onPressed: () async => Navigator.of(context).push(
187+
MaterialPageRoute<void>(
188+
builder: (BuildContext context) => PricesPage(
189+
_model!,
190+
pricesResult: _productPriceRefresher!.pricesResult,
191+
),
192+
),
193+
),
194+
),
194195
);
195196
}
196197
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import 'dart:async';
2+
import 'dart:ui';
3+
4+
import 'package:openfoodfacts/openfoodfacts.dart';
5+
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
6+
import 'package:smooth_app/database/local_database.dart';
7+
import 'package:smooth_app/pages/prices/get_prices_model.dart';
8+
import 'package:smooth_app/pages/product/common/loading_status.dart';
9+
import 'package:smooth_app/query/product_query.dart';
10+
11+
/// Async refresh of the prices of a product, with several loading phases.
12+
class ProductPriceRefresher {
13+
ProductPriceRefresher({
14+
required this.pricesResult,
15+
required this.model,
16+
required this.userPreferences,
17+
required this.refreshDisplay,
18+
});
19+
20+
final GetPricesModel model;
21+
final UserPreferences userPreferences;
22+
final VoidCallback refreshDisplay;
23+
24+
GetPricesResult? pricesResult;
25+
26+
LoadingStatus? _loadingStatus;
27+
28+
LoadingStatus? get loadingStatus => _loadingStatus;
29+
30+
String? _loadingError;
31+
32+
String? get loadingError => _loadingError;
33+
34+
String? get _barcode => model.parameters.productCode;
35+
36+
Future<void> runIfNeeded() async {
37+
_resetIfNeedsUpdate();
38+
if (loadingStatus == null) {
39+
return _asyncLoad();
40+
}
41+
}
42+
43+
void _resetIfNeedsUpdate() {
44+
if (_barcode == null) {
45+
return;
46+
}
47+
final int? latestUpdate = _latestUpdates[_barcode!];
48+
final int? latestRefresh = _latestRefreshes[_barcode!];
49+
if (latestRefresh != null &&
50+
latestUpdate != null &&
51+
latestUpdate > latestRefresh &&
52+
_loadingStatus != LoadingStatus.LOADING) {
53+
_loadingStatus = null;
54+
pricesResult = null;
55+
}
56+
}
57+
58+
Future<void> _asyncLoad() async {
59+
_loadingStatus = LoadingStatus.LOADING;
60+
refreshDisplay();
61+
final MaybeError<GetPricesResult> result;
62+
if (pricesResult != null) {
63+
result = MaybeError<GetPricesResult>.value(pricesResult!);
64+
} else {
65+
result = await OpenPricesAPIClient.getPrices(
66+
model.parameters,
67+
uriHelper: ProductQuery.uriPricesHelper,
68+
);
69+
}
70+
if (result.isError) {
71+
_loadingError = result.detailError;
72+
_loadingStatus = LoadingStatus.ERROR;
73+
} else {
74+
pricesResult = result.value;
75+
_loadingStatus = LoadingStatus.LOADED;
76+
if (model.lazyCounterPrices != null && pricesResult!.total != null) {
77+
model.lazyCounterPrices!.setLocalCount(
78+
pricesResult!.total!,
79+
userPreferences,
80+
notify: true,
81+
);
82+
}
83+
if (_barcode != null) {
84+
_setLatestRefresh(_barcode!);
85+
}
86+
}
87+
refreshDisplay();
88+
}
89+
90+
static final Map<String, int> _latestUpdates = <String, int>{};
91+
static final Map<String, int> _latestRefreshes = <String, int>{};
92+
93+
static void setLatestUpdate(final String barcode) =>
94+
_latestUpdates[barcode] = _getTimestamp();
95+
96+
static void _setLatestRefresh(final String barcode) =>
97+
_latestRefreshes[barcode] = _getTimestamp();
98+
99+
static int _getTimestamp() => LocalDatabase.nowInMillis();
100+
}

0 commit comments

Comments
 (0)