Skip to content

Commit d1d6392

Browse files
authored
Merge pull request #904 from wger-project/feature/cache-control
Improve exercise cache control
2 parents c89ccb6 + 47d782f commit d1d6392

11 files changed

+191
-24
lines changed

lib/providers/exercises.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ExercisesProvider with ChangeNotifier {
4747
static const EXERCISE_CACHE_DAYS = 7;
4848
static const CACHE_VERSION = 4;
4949

50+
static const exerciseUrlPath = 'exercise';
5051
static const exerciseInfoUrlPath = 'exerciseinfo';
5152
static const exerciseSearchPath = 'exercise/search';
5253

@@ -274,6 +275,18 @@ class ExercisesProvider with ChangeNotifier {
274275
}
275276
}
276277

278+
Future<void> fetchAndSetAllExercises() async {
279+
_logger.info('Loading all exercises from API');
280+
final exerciseData = await baseProvider.fetchPaginated(
281+
baseProvider.makeUrl(exerciseUrlPath, query: {'limit': API_MAX_PAGE_SIZE}),
282+
);
283+
final exerciseIds = exerciseData.map<int>((e) => e['id'] as int).toSet();
284+
285+
for (final exerciseId in exerciseIds) {
286+
await handleUpdateExerciseFromApi(database, exerciseId);
287+
}
288+
}
289+
277290
/// Returns the exercise with the given ID
278291
///
279292
/// If the exercise is not known locally, it is fetched from the server.
@@ -291,6 +304,7 @@ class ExercisesProvider with ChangeNotifier {
291304

292305
return exercise;
293306
} on NoSuchEntryException {
307+
// _logger.finer('Exercise not found locally, fetching from the API');
294308
return handleUpdateExerciseFromApi(database, exerciseId);
295309
}
296310
}

lib/providers/routines.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,14 @@ class RoutinesProvider with ChangeNotifier {
153153
/// Fetches and sets all workout plans fully, i.e. with all corresponding child
154154
/// attributes
155155
Future<void> fetchAndSetAllRoutinesFull() async {
156-
final data = await baseProvider.fetch(
156+
_logger.fine('Fetching all routines fully');
157+
final data = await baseProvider.fetchPaginated(
157158
baseProvider.makeUrl(
158159
_routinesUrlPath,
159160
query: {'ordering': '-creation_date', 'limit': API_MAX_PAGE_SIZE, 'is_template': 'false'},
160161
),
161162
);
162-
for (final entry in data['results']) {
163+
for (final entry in data) {
163164
await fetchAndSetRoutineFull(entry['id']);
164165
}
165166

@@ -169,6 +170,7 @@ class RoutinesProvider with ChangeNotifier {
169170
/// Fetches all routines sparsely, i.e. only with the data on the object itself
170171
/// and no child attributes
171172
Future<void> fetchAndSetAllRoutinesSparse() async {
173+
_logger.fine('Fetching all routines sparsely');
172174
final data = await baseProvider.fetch(
173175
baseProvider.makeUrl(
174176
_routinesUrlPath,

lib/screens/home_tabs_screen.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
8686
final measurementProvider = context.read<MeasurementProvider>();
8787
final userProvider = context.read<UserProvider>();
8888

89+
//
8990
// Base data
9091
widget._logger.info('Loading base data');
9192
await Future.wait([
@@ -95,7 +96,18 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
9596
nutritionPlansProvider.fetchIngredientsFromCache(),
9697
exercisesProvider.fetchAndSetInitialData(),
9798
]);
99+
exercisesProvider.fetchAndSetAllExercises();
100+
101+
// Workaround for https://github.com/wger-project/flutter/issues/901
102+
// It seems that it can happen that sometimes the units were not loaded properly
103+
// so now we check and try again if necessary. We might need a better general
104+
// solution since this could potentially happen with other data as well.
105+
if (routinesProvider.repetitionUnits.isEmpty || routinesProvider.weightUnits.isEmpty) {
106+
widget._logger.info('Routine units are empty, fetching again');
107+
await routinesProvider.fetchAndSetUnits();
108+
}
98109

110+
//
99111
// Plans, weight and gallery
100112
widget._logger.info('Loading routines, weight, measurements and gallery');
101113
await Future.wait([
@@ -107,13 +119,15 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
107119
measurementProvider.fetchAndSetAllCategoriesAndEntries(),
108120
]);
109121

122+
//
110123
// Current nutritional plan
111124
widget._logger.info('Loading current nutritional plan');
112125
if (nutritionPlansProvider.currentPlan != null) {
113126
final plan = nutritionPlansProvider.currentPlan!;
114127
await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!);
115128
}
116129

130+
//
117131
// Current routine
118132
widget._logger.info('Loading current routine');
119133
if (routinesProvider.currentRoutine != null) {

lib/widgets/core/settings.dart

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
import 'package:flutter/material.dart';
2121
import 'package:provider/provider.dart';
2222
import 'package:wger/l10n/generated/app_localizations.dart';
23-
import 'package:wger/providers/exercises.dart';
2423
import 'package:wger/providers/nutrition.dart';
2524
import 'package:wger/providers/user.dart';
2625
import 'package:wger/screens/configure_plates_screen.dart';
26+
import 'package:wger/widgets/core/settings/exercise_cache.dart';
2727

2828
class SettingsPage extends StatelessWidget {
2929
static String routeName = '/SettingsPage';
@@ -33,7 +33,6 @@ class SettingsPage extends StatelessWidget {
3333
@override
3434
Widget build(BuildContext context) {
3535
final i18n = AppLocalizations.of(context);
36-
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
3736
final nutritionProvider = Provider.of<NutritionPlansProvider>(context, listen: false);
3837
final userProvider = Provider.of<UserProvider>(context);
3938

@@ -47,24 +46,7 @@ class SettingsPage extends StatelessWidget {
4746
style: Theme.of(context).textTheme.headlineSmall,
4847
),
4948
),
50-
ListTile(
51-
title: Text(i18n.settingsExerciseCacheDescription),
52-
trailing: IconButton(
53-
key: const ValueKey('cacheIconExercises'),
54-
icon: const Icon(Icons.delete),
55-
onPressed: () async {
56-
await exerciseProvider.clearAllCachesAndPrefs();
57-
58-
if (context.mounted) {
59-
final snackBar = SnackBar(
60-
content: Text(i18n.settingsCacheDeletedSnackbar),
61-
);
62-
63-
ScaffoldMessenger.of(context).showSnackBar(snackBar);
64-
}
65-
},
66-
),
67-
),
49+
const SettingsExerciseCache(),
6850
ListTile(
6951
title: Text(i18n.settingsIngredientCacheDescription),
7052
trailing: IconButton(
@@ -83,6 +65,12 @@ class SettingsPage extends StatelessWidget {
8365
},
8466
),
8567
),
68+
ListTile(
69+
title: Text(
70+
i18n.others,
71+
style: Theme.of(context).textTheme.headlineSmall,
72+
),
73+
),
8674
ListTile(
8775
title: Text(i18n.themeMode),
8876
trailing: DropdownButton<ThemeMode>(
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'package:wger/l10n/generated/app_localizations.dart';
4+
import 'package:wger/providers/exercises.dart';
5+
6+
class SettingsExerciseCache extends StatefulWidget {
7+
const SettingsExerciseCache({super.key});
8+
9+
@override
10+
State<SettingsExerciseCache> createState() => _SettingsExerciseCacheState();
11+
}
12+
13+
class _SettingsExerciseCacheState extends State<SettingsExerciseCache> {
14+
bool _isRefreshLoading = false;
15+
String _subtitle = '';
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
20+
final i18n = AppLocalizations.of(context);
21+
22+
return ListTile(
23+
enabled: !_isRefreshLoading,
24+
title: Text(i18n.settingsExerciseCacheDescription),
25+
subtitle: _subtitle.isNotEmpty ? Text(_subtitle) : null,
26+
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
27+
IconButton(
28+
key: const ValueKey('cacheIconExercisesRefresh'),
29+
icon: _isRefreshLoading
30+
? const SizedBox(
31+
width: 24,
32+
height: 24,
33+
child: CircularProgressIndicator(strokeWidth: 2),
34+
)
35+
: const Icon(Icons.refresh),
36+
onPressed: _isRefreshLoading
37+
? null
38+
: () async {
39+
setState(() => _isRefreshLoading = true);
40+
41+
// Note: status messages are currently left in English on purpose
42+
try {
43+
setState(() => _subtitle = 'Clearing cache...');
44+
await exerciseProvider.clearAllCachesAndPrefs();
45+
46+
if (mounted) {
47+
setState(() => _subtitle = 'Loading languages and units...');
48+
}
49+
await exerciseProvider.fetchAndSetInitialData();
50+
51+
if (mounted) {
52+
setState(() => _subtitle = 'Loading all exercises from server...');
53+
}
54+
await exerciseProvider.fetchAndSetAllExercises();
55+
56+
if (mounted) {
57+
setState(() => _subtitle = '');
58+
}
59+
} finally {
60+
if (mounted) {
61+
setState(() => _isRefreshLoading = false);
62+
}
63+
if (context.mounted) {
64+
ScaffoldMessenger.of(context).showSnackBar(
65+
SnackBar(content: Text(i18n.success)),
66+
);
67+
}
68+
}
69+
},
70+
),
71+
IconButton(
72+
key: const ValueKey('cacheIconExercisesDelete'),
73+
icon: const Icon(Icons.delete),
74+
onPressed: () async {
75+
await exerciseProvider.clearAllCachesAndPrefs();
76+
77+
if (context.mounted) {
78+
final snackBar = SnackBar(
79+
content: Text(i18n.settingsCacheDeletedSnackbar),
80+
);
81+
82+
ScaffoldMessenger.of(context).showSnackBar(snackBar);
83+
}
84+
},
85+
)
86+
]),
87+
);
88+
}
89+
}

test/core/settings_test.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import 'settings_test.mocks.dart';
3939
WgerBaseProvider,
4040
SharedPreferencesAsync,
4141
])
42-
void main() async {
42+
void main() {
4343
final mockExerciseProvider = MockExercisesProvider();
4444
final mockNutritionProvider = MockNutritionPlansProvider();
4545
final mockSharedPreferences = MockSharedPreferencesAsync();
@@ -68,12 +68,22 @@ void main() async {
6868
group('Cache', () {
6969
testWidgets('Test resetting the exercise cache', (WidgetTester tester) async {
7070
await tester.pumpWidget(createSettingsScreen());
71-
await tester.tap(find.byKey(const ValueKey('cacheIconExercises')));
71+
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesDelete')));
7272
await tester.pumpAndSettle();
7373

7474
verify(mockExerciseProvider.clearAllCachesAndPrefs());
7575
});
7676

77+
testWidgets('Test refreshing the exercise cache', (WidgetTester tester) async {
78+
await tester.pumpWidget(createSettingsScreen());
79+
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesRefresh')));
80+
await tester.pumpAndSettle();
81+
82+
verify(mockExerciseProvider.clearAllCachesAndPrefs());
83+
verify(mockExerciseProvider.fetchAndSetInitialData());
84+
verify(mockExerciseProvider.fetchAndSetAllExercises());
85+
});
86+
7787
testWidgets('Test resetting the ingredient cache', (WidgetTester tester) async {
7888
await tester.pumpWidget(createSettingsScreen());
7989
await tester.tap(find.byKey(const ValueKey('cacheIconIngredients')));

test/core/settings_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,16 @@ class MockExercisesProvider extends _i1.Mock implements _i17.ExercisesProvider {
490490
returnValueForMissingStub: _i18.Future<void>.value(),
491491
) as _i18.Future<void>);
492492

493+
@override
494+
_i18.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
495+
Invocation.method(
496+
#fetchAndSetAllExercises,
497+
[],
498+
),
499+
returnValue: _i18.Future<void>.value(),
500+
returnValueForMissingStub: _i18.Future<void>.value(),
501+
) as _i18.Future<void>);
502+
493503
@override
494504
_i18.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
495505
Invocation.method(

test/exercises/contribute_exercise_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,16 @@ class MockExercisesProvider extends _i1.Mock implements _i20.ExercisesProvider {
945945
returnValueForMissingStub: _i15.Future<void>.value(),
946946
) as _i15.Future<void>);
947947

948+
@override
949+
_i15.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
950+
Invocation.method(
951+
#fetchAndSetAllExercises,
952+
[],
953+
),
954+
returnValue: _i15.Future<void>.value(),
955+
returnValueForMissingStub: _i15.Future<void>.value(),
956+
) as _i15.Future<void>);
957+
948958
@override
949959
_i15.Future<_i3.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
950960
Invocation.method(

test/exercises/exercises_detail_widget_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ class MockExercisesProvider extends _i1.Mock implements _i9.ExercisesProvider {
377377
returnValueForMissingStub: _i10.Future<void>.value(),
378378
) as _i10.Future<void>);
379379

380+
@override
381+
_i10.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
382+
Invocation.method(
383+
#fetchAndSetAllExercises,
384+
[],
385+
),
386+
returnValue: _i10.Future<void>.value(),
387+
returnValueForMissingStub: _i10.Future<void>.value(),
388+
) as _i10.Future<void>);
389+
380390
@override
381391
_i10.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
382392
Invocation.method(

test/routine/gym_mode_screen_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,16 @@ class MockExercisesProvider extends _i1.Mock implements _i12.ExercisesProvider {
580580
returnValueForMissingStub: _i11.Future<void>.value(),
581581
) as _i11.Future<void>);
582582

583+
@override
584+
_i11.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
585+
Invocation.method(
586+
#fetchAndSetAllExercises,
587+
[],
588+
),
589+
returnValue: _i11.Future<void>.value(),
590+
returnValueForMissingStub: _i11.Future<void>.value(),
591+
) as _i11.Future<void>);
592+
583593
@override
584594
_i11.Future<_i6.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
585595
Invocation.method(

0 commit comments

Comments
 (0)