Skip to content

Commit

Permalink
fix: 5554 - display of cached counts on user page (#5573)
Browse files Browse the repository at this point in the history
New files:
* `lazy_counter.dart`: Lazy Counter, with a cached value stored locally, and a call to the server.
* `lazy_counter_widget.dart`: Widget displaying a Lazy Counter: cached value, refresh button, and loading.

Impacted files:
* `user_preferences.dart`: added methods about storing a lazy count value
* `user_preferences_account.dart`: refactored the display of counts with the new `LazyCounter` and `LazyCounterWidget` classes
  • Loading branch information
monsieurtanuki authored Sep 11, 2024
1 parent 51b2277 commit 74660f8
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 71 deletions.
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

0 comments on commit 74660f8

Please sign in to comment.