Improve exercise initial loading and reading from cache

This commit is contained in:
Roland Geider
2021-09-15 20:41:59 +02:00
parent 3bded2384b
commit f90ddc9366
6 changed files with 110 additions and 61 deletions

View File

@@ -41,8 +41,10 @@ const DAYS_TO_CACHE = 20;
/// Name of the submit button in forms
const SUBMIT_BUTTON_KEY_NAME = 'submit-button';
/// Local Preferences key for exercises
/// Local Preferences keys
const PREFS_EXERCISES = 'exerciseData';
const PREFS_EXERCISE_CACHE_VERSION = 'cacheVersion';
const PREFS_INGREDIENTS = 'ingredientData';
const DEFAULT_ANIMATION_DURATION = Duration(milliseconds: 200);
const DEFAULT_ANIMATION_CURVE = Curves.bounceIn;

View File

@@ -25,11 +25,15 @@ class Comment {
@JsonKey(required: true)
final int id;
@JsonKey(required: true, name: 'exercise')
final int exerciseId;
@JsonKey(required: true)
final String comment;
Comment({
required this.id,
required this.exerciseId,
required this.comment,
});

View File

@@ -7,14 +7,16 @@ part of 'comment.dart';
// **************************************************************************
Comment _$CommentFromJson(Map<String, dynamic> json) {
$checkKeys(json, requiredKeys: const ['id', 'comment']);
$checkKeys(json, requiredKeys: const ['id', 'exercise', 'comment']);
return Comment(
id: json['id'] as int,
exerciseId: json['exercise'] as int,
comment: json['comment'] as String,
);
}
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
'id': instance.id,
'exercise': instance.exerciseId,
'comment': instance.comment,
};

View File

@@ -18,6 +18,7 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:wger/models/exercises/alias.dart';
import 'package:wger/models/exercises/base.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/comment.dart';
@@ -60,6 +61,9 @@ class Exercise extends Equatable {
@JsonKey(ignore: true)
List<Comment> tips = [];
@JsonKey(ignore: true)
List<Alias> alias = [];
Exercise(
{required this.id,
required this.baseId,

View File

@@ -26,8 +26,10 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wger/exceptions/no_such_entry_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/exercises/alias.dart';
import 'package:wger/models/exercises/base.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/comment.dart';
import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/exercises/language.dart';
@@ -38,14 +40,17 @@ class ExercisesProvider with ChangeNotifier {
final WgerBaseProvider baseProvider;
static const EXERCISE_CACHE_DAYS = 7;
static const CACHE_VERSION = 2;
static const _exerciseInfoUrlPath = 'exerciseinfo';
static const _exerciseBaseUrlPath = 'exercise-base';
static const _exerciseUrlPath = 'exercise';
static const _exerciseSearchPath = 'exercise/search';
static const _exerciseVariationsUrlPath = 'variation';
static const _exerciseCommentUrlPath = 'exercisecomment';
static const _exerciseImagesUrlPath = 'exerciseimage';
static const _exerciseAliasUrlPath = 'exercisealias';
static const _categoriesUrlPath = 'exercisecategory';
static const _musclesUrlPath = 'muscle';
static const _equipmentUrlPath = 'equipment';
@@ -260,7 +265,7 @@ class ExercisesProvider with ChangeNotifier {
baseProvider.makeUrl(_exerciseBaseUrlPath, id: exercise.baseId),
);
final base = ExerciseBase.fromJson(exerciseBaseData);
setExerciseBaseData(base, [exercise]);
//setExerciseBaseData(base, [exercise]);
/*
final prefs = await SharedPreferences.getInstance();
@@ -288,21 +293,62 @@ class ExercisesProvider with ChangeNotifier {
}
}
/// Helper function that sets different objects such as category, etc.
ExerciseBase setExerciseBaseData(ExerciseBase base, List<Exercise> exercises) {
base.category = findCategoryById(base.categoryId);
base.muscles = base.musclesIds.map((e) => findMuscleById(e)).toList();
base.musclesSecondary = base.musclesSecondaryIds.map((e) => findMuscleById(e)).toList();
base.equipment = base.equipmentIds.map((e) => findEquipmentById(e)).toList();
List<ExerciseBase> mapBases(dynamic data, List<Exercise> exercises) {
try {
final bases = data.map<ExerciseBase>((e) {
final base = ExerciseBase.fromJson(e);
base.category = findCategoryById(base.categoryId);
base.muscles = base.musclesIds.map((e) => findMuscleById(e)).toList();
base.musclesSecondary = base.musclesSecondaryIds.map((e) => findMuscleById(e)).toList();
base.equipment = base.equipmentIds.map((e) => findEquipmentById(e)).toList();
exercises.forEach((e) {
e.base = base;
e.language = findLanguageById(e.languageId);
exercises.forEach((e) {
e.base = base;
e.language = findLanguageById(e.languageId);
});
base.exercises = exercises;
return base;
});
return bases.toList();
} catch (e) {
print(e);
print('********************');
throw e;
}
}
List<Exercise> mapAliases(dynamic data, List<Exercise> exercises) {
List<Alias> alias = data.map<Alias>((e) => Alias.fromJson(e)).toList();
alias.forEach((e) {
exercises.firstWhere((exercise) => exercise.id == e.exerciseId).alias.add(e);
});
base.exercises = [];
base.exercises = exercises;
return exercises;
}
return base;
List<Exercise> mapComments(dynamic data, List<Exercise> exercises) {
List<Comment> comments = data.map<Comment>((e) => Comment.fromJson(e)).toList();
comments.forEach((e) {
exercises.firstWhere((exercise) => exercise.id == e.exerciseId).tips.add(e);
});
return exercises;
}
/// Checks the required cache version
///
/// This is needed since the content of the exercise cache can change and we need
/// to invalidate it as a result
checkExerciseCacheVersion() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey(PREFS_EXERCISE_CACHE_VERSION)) {
final cacheVersion = prefs.getInt(PREFS_EXERCISE_CACHE_VERSION);
if (cacheVersion! != CACHE_VERSION) {
prefs.remove(PREFS_EXERCISES);
}
prefs.setInt(PREFS_EXERCISE_CACHE_VERSION, CACHE_VERSION);
} else {
prefs.remove(PREFS_EXERCISES);
prefs.setInt(PREFS_EXERCISE_CACHE_VERSION, CACHE_VERSION);
}
}
Future<void> fetchAndSetExercises() async {
@@ -311,6 +357,7 @@ class ExercisesProvider with ChangeNotifier {
print(Intl.shortLocale(Intl.getCurrentLocale()));
print('---------');
final prefs = await SharedPreferences.getInstance();
await checkExerciseCacheVersion();
if (prefs.containsKey(PREFS_EXERCISES)) {
final cacheData = json.decode(prefs.getString(PREFS_EXERCISES)!);
@@ -319,14 +366,12 @@ class ExercisesProvider with ChangeNotifier {
cacheData['muscles'].forEach((e) => _muscles.add(Muscle.fromJson(e)));
cacheData['categories'].forEach((e) => _categories.add(ExerciseCategory.fromJson(e)));
cacheData['languages'].forEach((e) => _languages.add(Language.fromJson(e)));
cacheData['exercise-translations'].forEach((e) => _exercises.add(Exercise.fromJson(e)));
cacheData['bases'].forEach((e) {
var base = setExerciseBaseData(
ExerciseBase.fromJson(e),
_exercises.where((element) => element.baseId == e['id']).toList(),
);
_exerciseBases.add(base);
});
List<Exercise> exercises =
cacheData['exercise-translations'].map<Exercise>((e) => Exercise.fromJson(e)).toList();
exercises = mapComments(cacheData['exercise-comments'], exercises);
_exercises = mapAliases(cacheData['exercise-alias'], exercises);
_exerciseBases = mapBases(cacheData['bases'], _exercises);
_initFilters();
log("Read ${_exerciseBases.length} exercises from cache. Valid till ${cacheData['expiresIn']}");
return;
@@ -334,44 +379,28 @@ class ExercisesProvider with ChangeNotifier {
}
// Load categories, muscles, equipment and languages
await Future.wait([
final data = await Future.wait<dynamic>([
baseProvider.fetch(baseProvider.makeUrl(_exerciseBaseUrlPath, query: {'limit': '1000'})),
baseProvider.fetch(baseProvider.makeUrl(_exerciseUrlPath, query: {'limit': '1000'})),
baseProvider.fetch(baseProvider.makeUrl(_exerciseCommentUrlPath, query: {'limit': '1000'})),
baseProvider.fetch(baseProvider.makeUrl(_exerciseAliasUrlPath, query: {'limit': '1000'})),
fetchAndSetCategories(),
fetchAndSetMuscles(),
fetchAndSetEquipment(),
fetchAndSetLanguages(),
]);
final exerciseBaseData = data[0]['results'];
final exerciseData = data[1]['results'];
final commentsData = data[2]['results'];
final aliasData = data[3]['results'];
final exerciseBaseData = await baseProvider.fetch(
baseProvider.makeUrl(_exerciseBaseUrlPath, query: {'limit': '1000'}),
);
final exerciseTranslationData = await baseProvider.fetch(
baseProvider.makeUrl(
_exerciseUrlPath,
query: {'limit': '1000'},
),
);
List<Exercise> exerciseTranslation = exerciseTranslationData['results'].map<Exercise>((e) {
return Exercise.fromJson(e);
}).toList();
for (var e in exerciseBaseData['results']) {
var base = setExerciseBaseData(
ExerciseBase.fromJson(e),
exerciseTranslation.where((element) => element.baseId == e['id']).toList(),
);
_exerciseBases.add(base);
}
// Load exercise
List<Exercise> exercises = exerciseData.map<Exercise>((e) => Exercise.fromJson(e)).toList();
exercises = mapComments(commentsData, exercises);
_exercises = mapAliases(aliasData, exercises);
_exerciseBases = mapBases(exerciseBaseData, _exercises);
try {
List<Exercise> exerciseTranslations = [];
_exerciseBases.forEach((base) {
base.exercises.forEach((exercise) {
exerciseTranslations.add(exercise);
});
});
// Save the result to the cache
final cacheData = {
'date': DateTime.now().toIso8601String(),
@@ -380,8 +409,10 @@ class ExercisesProvider with ChangeNotifier {
'categories': _categories.map((e) => e.toJson()).toList(),
'muscles': _muscles.map((e) => e.toJson()).toList(),
'languages': _languages.map((e) => e.toJson()).toList(),
'exercise-translations': exerciseTranslations.map((e) => e.toJson()).toList(),
'bases': _exerciseBases.map((e) => e.toJson()).toList(),
'bases': exerciseBaseData,
'exercise-translations': exerciseData,
'exercise-comments': commentsData,
'exercise-alias': aliasData,
};
log("Saved ${_exercises.length} exercises from cache. Valid till ${cacheData['expiresIn']}");
@@ -391,6 +422,8 @@ class ExercisesProvider with ChangeNotifier {
} on MissingRequiredKeysException catch (error) {
log(error.missingKeys.toString());
throw (error);
} catch (e) {
throw (e);
}
}

View File

@@ -23,8 +23,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/exceptions/no_such_entry_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/models/nutrition/log.dart';
import 'package:wger/models/nutrition/meal.dart';
@@ -68,7 +69,10 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
}
NutritionalPlan findById(int id) {
return _plans.firstWhere((plan) => plan.id == id);
return _plans.firstWhere(
(plan) => plan.id == id,
orElse: () => throw NoSuchEntryException(),
);
}
Meal? findMealById(int id) {
@@ -120,7 +124,7 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
NutritionalPlan plan;
try {
plan = findById(planId);
} on StateError catch (e) {
} on NoSuchEntryException catch (e) {
plan = await fetchAndSetPlanSparse(planId);
}
@@ -280,8 +284,8 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
Future<void> fetchIngredientsFromCache() async {
// Load exercises from cache, if available
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('ingredientData')) {
final ingredientData = json.decode(prefs.getString('ingredientData')!);
if (prefs.containsKey(PREFS_INGREDIENTS)) {
final ingredientData = json.decode(prefs.getString(PREFS_INGREDIENTS)!);
if (DateTime.parse(ingredientData['expiresIn']).isAfter(DateTime.now())) {
ingredientData['ingredients'].forEach((e) => _ingredients.add(Ingredient.fromJson(e)));
log("Read ${ingredientData['ingredients'].length} ingredients from cache. Valid till ${ingredientData['expiresIn']}");
@@ -295,7 +299,7 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
'expiresIn': DateTime.now().add(Duration(days: DAYS_TO_CACHE)).toIso8601String(),
'ingredients': []
};
prefs.setString('ingredientData', json.encode(ingredientData));
prefs.setString(PREFS_INGREDIENTS, json.encode(ingredientData));
return;
}