Skip to content

Commit 7b542ba

Browse files
committed
feat: add request caching service (TBD54566975#280)
feat: add tests for request caching service (TBD54566975#280) fix: fix box import and tests for request caching service (TBD54566975#280)
1 parent 0bc1feb commit 7b542ba

File tree

2 files changed

+471
-0
lines changed

2 files changed

+471
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:developer';
4+
5+
import 'package:collection/collection.dart';
6+
import 'package:hive/hive.dart';
7+
import 'package:http/http.dart' as http;
8+
9+
class RequestCacheService<T> {
10+
final T Function(Map<String, dynamic>) fromJson;
11+
final Map<String, dynamic> Function(T) toJson;
12+
final Duration cacheDuration;
13+
final String networkCacheKey;
14+
final http.Client httpClient;
15+
final bool checkForUpdates;
16+
final Box cacheBox;
17+
18+
RequestCacheService({
19+
required this.fromJson,
20+
required this.toJson,
21+
required this.cacheBox,
22+
this.cacheDuration = const Duration(minutes: 1),
23+
this.networkCacheKey = 'network_cache',
24+
http.Client? httpClient,
25+
this.checkForUpdates = false,
26+
}) : httpClient = httpClient ?? http.Client();
27+
28+
/// Fetches data from the cache and API.
29+
Stream<T> fetchData(String url) async* {
30+
final cacheKey = url;
31+
bool cacheEmitted = false;
32+
33+
try {
34+
// Check for cached data
35+
final cachedEntry = await cacheBox.get(cacheKey);
36+
if (cachedEntry != null && cachedEntry is Map) {
37+
final cachedMap = Map<String, dynamic>.from(cachedEntry);
38+
final cachedTimestamp =
39+
DateTime.parse(cachedMap['timestamp'] as String);
40+
final cachedDataJson =
41+
Map<String, dynamic>.from(cachedMap['data'] ?? {});
42+
final cachedData = fromJson(cachedDataJson);
43+
// Emit cached data
44+
yield cachedData;
45+
cacheEmitted = true;
46+
47+
final now = DateTime.now();
48+
// Decide whether to fetch new data based on cache validity
49+
if (now.difference(cachedTimestamp) < cacheDuration &&
50+
!checkForUpdates) {
51+
// Cache is still valid, but we'll fetch new data to check for updates
52+
return;
53+
}
54+
}
55+
56+
// Fetch data from the network
57+
try {
58+
final response = await httpClient.get(Uri.parse(url));
59+
60+
if (response.statusCode == 200) {
61+
final dataMap = jsonDecode(response.body) as Map<String, dynamic>;
62+
final data = fromJson(dataMap);
63+
64+
// Compare with cached data
65+
bool isDataUpdated = true;
66+
if (cachedEntry != null && cachedEntry is Map) {
67+
final cachedDataJson =
68+
Map<String, dynamic>.from(cachedEntry['data'] ?? {});
69+
// Use DeepCollectionEquality for deep comparison
70+
const equalityChecker = DeepCollectionEquality();
71+
if (equalityChecker.equals(cachedDataJson, dataMap)) {
72+
isDataUpdated = false;
73+
}
74+
}
75+
76+
// Cache the new data with a timestamp
77+
final cacheEntry = {
78+
'data': toJson(data),
79+
'timestamp': DateTime.now().toIso8601String(),
80+
};
81+
await cacheBox.put(cacheKey, cacheEntry);
82+
83+
if (isDataUpdated) {
84+
yield data;
85+
}
86+
} else {
87+
throw Exception(
88+
'Failed to load data from $url. Status code: ${response.statusCode}',
89+
);
90+
}
91+
} catch (e) {
92+
if (!cacheEmitted) {
93+
// No cached data was emitted before, so we need to throw an error
94+
throw Exception('Error fetching data from $url: $e');
95+
}
96+
// Else, we have already emitted cached data, so we can silently fail or log the error
97+
}
98+
} catch (e) {
99+
if (!cacheEmitted) {
100+
// No cached data was emitted before, so we need to throw an error
101+
throw Exception('Error fetching data from $url: $e');
102+
}
103+
// Else, we have already emitted cached data, so we can silently fail or log the error
104+
log('Error fetching data from $url: $e');
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)