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: 5554 - display of cached counts on user page #5573

Merged
merged 2 commits into from
Sep 11, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class UserPreferences extends ChangeNotifier {
static const String _TAG_EXCLUDED_ATTRIBUTE_IDS = 'excluded_attributes';
static const String _TAG_USER_GROUP = '_user_group';
static const String _TAG_UNIQUE_RANDOM = '_unique_random';
static const String _TAG_LAZY_COUNT_PREFIX = '_lazy_count_prefix';

/// Camera preferences

Expand Down Expand Up @@ -173,6 +174,14 @@ class UserPreferences extends ChangeNotifier {
notifyListeners();
}

String _getLazyCountTag(final String tag) => '$_TAG_LAZY_COUNT_PREFIX$tag';

Future<void> setLazyCount(final int value, final String suffixTag) async =>
_sharedPreferences.setInt(_getLazyCountTag(suffixTag), value);

int? getLazyCount(final String suffixTag) =>
_sharedPreferences.getInt(_getLazyCountTag(suffixTag));

Future<void> setUserTracking(final bool state) async {
await _sharedPreferences.setBool(_TAG_USER_TRACKING, state);
onAnalyticsChanged.value = state;
Expand Down
93 changes: 93 additions & 0 deletions packages/smooth_app/lib/pages/preferences/lazy_counter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'package:flutter/foundation.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
import 'package:smooth_app/query/paged_user_product_query.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/services/smooth_services.dart';

/// Lazy Counter, with a cached value stored locally, and a call to the server.
abstract class LazyCounter {
const LazyCounter();

/// Returns the value cached locally;
int? getLocalCount(final UserPreferences userPreferences) =>
userPreferences.getLazyCount(getSuffixTag());

/// Sets the value cached locally;
Future<void> setLocalCount(
final int value,
final UserPreferences userPreferences,
) =>
userPreferences.setLazyCount(value, getSuffixTag());

/// Returns the suffix tag used to cache the value locally;
@protected
String getSuffixTag();

/// Gets the latest value from the server.
Future<int?> getServerCount();
}

/// Lazy Counter dedicated to Prices counts.
class LazyCounterPrices extends LazyCounter {
const LazyCounterPrices(this.owner);

final String? owner;

@override
String getSuffixTag() => 'P_$owner';

@override
Future<int?> getServerCount() async {
final MaybeError<GetPricesResult> result =
await OpenPricesAPIClient.getPrices(
GetPricesParameters()
..owner = owner
..pageSize = 1,
uriHelper: ProductQuery.uriPricesHelper,
);
if (result.isError) {
return null;
}
return result.value.total;
}
}

/// Lazy Counter dedicated to OFF User Search counts.
class LazyCounterUserSearch extends LazyCounter {
const LazyCounterUserSearch(this.type);

final UserSearchType type;

@override
String getSuffixTag() => 'US_$type';

@override
Future<int?> getServerCount() async {
final User user = ProductQuery.getWriteUser();
final ProductSearchQueryConfiguration configuration = type.getConfiguration(
user.userId,
1,
1,
ProductQuery.getLanguage(),
// one field is enough as we want only the count
// and we need at least one field (no field meaning all fields)
<ProductField>[ProductField.BARCODE],
);

try {
final SearchResult result = await OpenFoodAPIClient.searchProducts(
user,
configuration,
uriHelper: ProductQuery.uriProductHelper,
);
return result.count;
} catch (e) {
Logs.e(
'Could not count the number of products for $type, ${user.userId}',
ex: e,
);
return null;
}
}
}
78 changes: 78 additions & 0 deletions packages/smooth_app/lib/pages/preferences/lazy_counter_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
import 'package:smooth_app/pages/preferences/lazy_counter.dart';

/// Widget displaying a Lazy Counter: cached value, refresh button, and loading.
class LazyCounterWidget extends StatefulWidget {
const LazyCounterWidget(this.lazyCounter);

final LazyCounter lazyCounter;

@override
State<LazyCounterWidget> createState() => _LazyCounterWidgetState();
}

class _LazyCounterWidgetState extends State<LazyCounterWidget> {
bool _loading = false;
int? _count;

@override
void initState() {
super.initState();
final UserPreferences userPreferences = context.read<UserPreferences>();
_count = widget.lazyCounter.getLocalCount(userPreferences);
if (_count == null) {
_asyncLoad();
}
}

@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (_count != null) Text(_count.toString()),
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator.adaptive(),
),
)
else
IconButton(
onPressed: () => _asyncLoad(),
icon: const Icon(Icons.refresh),
),
],
);

Future<void> _asyncLoad() async {
if (_loading) {
return;
}
_loading = true;
final UserPreferences userPreferences = context.read<UserPreferences>();
if (mounted) {
setState(() {});
}
try {
final int? value = await widget.lazyCounter.getServerCount();
if (value != null) {
await widget.lazyCounter.setLocalCount(value, userPreferences);
_count = value;
}
} catch (e) {
//
} finally {
_loading = false;
if (mounted) {
setState(() {});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'package:smooth_app/helpers/launch_url_helper.dart';
import 'package:smooth_app/helpers/user_management_helper.dart';
import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart';
import 'package:smooth_app/pages/preferences/account_deletion_webview.dart';
import 'package:smooth_app/pages/preferences/lazy_counter.dart';
import 'package:smooth_app/pages/preferences/lazy_counter_widget.dart';
import 'package:smooth_app/pages/preferences/user_preferences_item.dart';
import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart';
import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
Expand All @@ -29,7 +31,6 @@ import 'package:smooth_app/query/paged_product_query.dart';
import 'package:smooth_app/query/paged_to_be_completed_product_query.dart';
import 'package:smooth_app/query/paged_user_product_query.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/services/smooth_services.dart';

class UserPreferencesAccount extends AbstractUserPreferences {
UserPreferencesAccount({
Expand Down Expand Up @@ -182,7 +183,7 @@ class UserPreferencesAccount extends AbstractUserPreferences {
iconData: Icons.add_circle_outline,
context: context,
localDatabase: localDatabase,
myCount: _getMyCount(UserSearchType.CONTRIBUTOR),
lazyCounter: const LazyCounterUserSearch(UserSearchType.CONTRIBUTOR),
),
_buildProductQueryTile(
productQuery: PagedUserProductQuery(
Expand All @@ -193,7 +194,7 @@ class UserPreferencesAccount extends AbstractUserPreferences {
iconData: Icons.edit,
context: context,
localDatabase: localDatabase,
myCount: _getMyCount(UserSearchType.INFORMER),
lazyCounter: const LazyCounterUserSearch(UserSearchType.INFORMER),
),
_buildProductQueryTile(
productQuery: PagedUserProductQuery(
Expand All @@ -204,7 +205,7 @@ class UserPreferencesAccount extends AbstractUserPreferences {
iconData: Icons.add_a_photo,
context: context,
localDatabase: localDatabase,
myCount: _getMyCount(UserSearchType.PHOTOGRAPHER),
lazyCounter: const LazyCounterUserSearch(UserSearchType.PHOTOGRAPHER),
),
_buildProductQueryTile(
productQuery: PagedUserProductQuery(
Expand All @@ -215,7 +216,8 @@ class UserPreferencesAccount extends AbstractUserPreferences {
iconData: Icons.more_horiz,
context: context,
localDatabase: localDatabase,
myCount: _getMyCount(UserSearchType.TO_BE_COMPLETED),
lazyCounter:
const LazyCounterUserSearch(UserSearchType.TO_BE_COMPLETED),
),
_getListTile(
PriceUserButton.showUserTitle(
Expand All @@ -227,7 +229,7 @@ class UserPreferencesAccount extends AbstractUserPreferences {
context: context,
),
CupertinoIcons.money_dollar_circle,
myCount: _getPricesCount(owner: ProductQuery.getWriteUser().userId),
lazyCounter: LazyCounterPrices(ProductQuery.getWriteUser().userId),
),
_getListTile(
appLocalizations.user_search_proofs_title,
Expand Down Expand Up @@ -281,7 +283,7 @@ class UserPreferencesAccount extends AbstractUserPreferences {
),
),
CupertinoIcons.money_dollar_circle,
myCount: _getPricesCount(),
lazyCounter: const LazyCounterPrices(null),
),
_getListTile(
appLocalizations.all_search_prices_top_user_title,
Expand Down Expand Up @@ -386,57 +388,13 @@ class UserPreferencesAccount extends AbstractUserPreferences {
},
);

Future<int?> _getMyCount(
final UserSearchType type,
) async {
final User user = ProductQuery.getWriteUser();
final ProductSearchQueryConfiguration configuration = type.getConfiguration(
user.userId,
1,
1,
ProductQuery.getLanguage(),
// one field is enough as we want only the count
// and we need at least one field (no field meaning all fields)
<ProductField>[ProductField.BARCODE],
);

try {
final SearchResult result = await OpenFoodAPIClient.searchProducts(
user,
configuration,
uriHelper: ProductQuery.uriProductHelper,
);
return result.count;
} catch (e) {
Logs.e(
'Could not count the number of products for $type, ${user.userId}',
ex: e,
);
return null;
}
}

Future<int?> _getPricesCount({final String? owner}) async {
final MaybeError<GetPricesResult> result =
await OpenPricesAPIClient.getPrices(
GetPricesParameters()
..owner = owner
..pageSize = 1,
uriHelper: ProductQuery.uriPricesHelper,
);
if (result.isError) {
return null;
}
return result.value.total;
}

UserPreferencesItem _buildProductQueryTile({
required final PagedProductQuery productQuery,
required final String title,
required final IconData iconData,
required final BuildContext context,
required final LocalDatabase localDatabase,
final Future<int?>? myCount,
final LazyCounter? lazyCounter,
}) =>
_getListTile(
title,
Expand All @@ -448,14 +406,14 @@ class UserPreferencesAccount extends AbstractUserPreferences {
editableAppBarTitle: false,
),
iconData,
myCount: myCount,
lazyCounter: lazyCounter,
);

UserPreferencesItem _getListTile(
final String title,
final VoidCallback onTap,
final IconData leading, {
final Future<int?>? myCount,
final LazyCounter? lazyCounter,
}) =>
UserPreferencesItemSimple(
labels: <String>[title],
Expand All @@ -472,23 +430,8 @@ class UserPreferencesAccount extends AbstractUserPreferences {
borderRadius: BorderRadius.circular(15),
),
leading: UserPreferencesListTile.getTintedIcon(leading, context),
trailing: myCount == null
? null
: FutureBuilder<int?>(
future: myCount,
builder:
(BuildContext context, AsyncSnapshot<int?> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox(
height: LARGE_SPACE,
width: LARGE_SPACE,
child: CircularProgressIndicator.adaptive());
}
return snapshot.data == null
? EMPTY_WIDGET
: Text(snapshot.data.toString());
},
),
trailing:
lazyCounter == null ? null : LazyCounterWidget(lazyCounter),
),
),
);
Expand Down
Loading