Skip to content

Commit 1cf2907

Browse files
committed
feat(caching): implement cache manager for offline first access
1 parent a3d6e52 commit 1cf2907

File tree

7 files changed

+213
-7
lines changed

7 files changed

+213
-7
lines changed

lib/core/network/api_client.dart

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import 'dart:async';
12
import 'package:dio/dio.dart';
23
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4+
import 'package:connectivity_plus/connectivity_plus.dart';
5+
import 'package:progres/core/network/cache_manager.dart';
36

47
class ApiClient {
58
static const String baseUrl = 'https://progres.mesrs.dz/api';
69
late final Dio _dio;
710
final FlutterSecureStorage _secureStorage;
11+
late final CacheManager _cacheManager;
12+
final Duration _shortTimeout = const Duration(seconds: 5);
13+
final Connectivity _connectivity = Connectivity();
814

915
ApiClient({FlutterSecureStorage? secureStorage})
1016
: _secureStorage = secureStorage ?? const FlutterSecureStorage() {
@@ -31,6 +37,12 @@ class ApiClient {
3137
},
3238
),
3339
);
40+
CacheManager.getInstance().then((value) => _cacheManager = value);
41+
}
42+
43+
Future<bool> get isConnected async {
44+
final result = await _connectivity.checkConnectivity();
45+
return result != ConnectivityResult.none;
3446
}
3547

3648
Future<void> saveToken(String token) async {
@@ -64,14 +76,61 @@ class ApiClient {
6476
await _secureStorage.delete(key: 'etablissement_id');
6577
}
6678

79+
// Generate a cache key string based on path and query parameters
80+
String _cacheKey(String path, Map<String, dynamic>? queryParameters) {
81+
final queryStr =
82+
queryParameters != null
83+
? Uri(queryParameters: queryParameters).query
84+
: '';
85+
return '$path?$queryStr';
86+
}
87+
6788
Future<Response> get(
6889
String path, {
6990
Map<String, dynamic>? queryParameters,
7091
}) async {
92+
final key = _cacheKey(path, queryParameters);
93+
94+
if (!await isConnected) {
95+
// offline - use cached data if available
96+
final cachedData = _cacheManager.getCache(key);
97+
if (cachedData != null) {
98+
return Response(
99+
requestOptions: RequestOptions(path: path),
100+
data: cachedData,
101+
statusCode: 200,
102+
);
103+
} else {
104+
// No cache, throw offline error
105+
throw DioException(
106+
requestOptions: RequestOptions(path: path),
107+
error: 'No internet connection and no cached data',
108+
);
109+
}
110+
}
111+
71112
try {
72-
final response = await _dio.get(path, queryParameters: queryParameters);
113+
// Try to get fresh data with a short timeout for fast fallback on slow responses
114+
final response = await _dio.get(
115+
path,
116+
queryParameters: queryParameters,
117+
options: Options(
118+
sendTimeout: _shortTimeout,
119+
receiveTimeout: _shortTimeout,
120+
),
121+
);
122+
await _cacheManager.saveCache(key, response.data);
73123
return response;
74124
} catch (e) {
125+
// On failure, return cached data if available
126+
final cachedData = _cacheManager.getCache(key);
127+
if (cachedData != null) {
128+
return Response(
129+
requestOptions: RequestOptions(path: path),
130+
data: cachedData,
131+
statusCode: 200,
132+
);
133+
}
75134
rethrow;
76135
}
77136
}
@@ -95,6 +154,24 @@ class WebApiClient extends ApiClient {
95154
String path, {
96155
Map<String, dynamic>? queryParameters,
97156
}) async {
157+
final key = _cacheKey(path, queryParameters);
158+
159+
if (!await isConnected) {
160+
final cachedData = _cacheManager.getCache(key);
161+
if (cachedData != null) {
162+
return Response(
163+
requestOptions: RequestOptions(path: path),
164+
data: cachedData,
165+
statusCode: 200,
166+
);
167+
} else {
168+
throw DioException(
169+
requestOptions: RequestOptions(path: path),
170+
error: 'No internet connection and no cached data',
171+
);
172+
}
173+
}
174+
98175
try {
99176
final Map<String, dynamic> proxyQueryParams = {
100177
'endpoint': "api" + path,
@@ -104,9 +181,22 @@ class WebApiClient extends ApiClient {
104181
final response = await _dio.get(
105182
proxyBaseUrl,
106183
queryParameters: proxyQueryParams,
184+
options: Options(
185+
sendTimeout: _shortTimeout,
186+
receiveTimeout: _shortTimeout,
187+
),
107188
);
189+
await _cacheManager.saveCache(key, response.data);
108190
return response;
109191
} catch (e) {
192+
final cachedData = _cacheManager.getCache(key);
193+
if (cachedData != null) {
194+
return Response(
195+
requestOptions: RequestOptions(path: path),
196+
data: cachedData,
197+
statusCode: 200,
198+
);
199+
}
110200
rethrow;
111201
}
112202
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'dart:convert';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
4+
class CacheManager {
5+
static const Duration cacheExpiry = Duration(hours: 24); // Cache valid for 24h
6+
7+
final SharedPreferences _prefs;
8+
9+
CacheManager._(this._prefs);
10+
11+
static Future<CacheManager> getInstance() async {
12+
final prefs = await SharedPreferences.getInstance();
13+
return CacheManager._(prefs);
14+
}
15+
16+
String _cacheKey(String key) => 'cache_\$key';
17+
18+
String _timestampKey(String key) => 'cache_timestamp_\$key';
19+
20+
Future<void> saveCache(String key, dynamic data) async {
21+
final jsonString = jsonEncode(data);
22+
await _prefs.setString(_cacheKey(key), jsonString);
23+
final timestamp = DateTime.now().millisecondsSinceEpoch;
24+
await _prefs.setInt(_timestampKey(key), timestamp);
25+
}
26+
27+
dynamic getCache(String key) {
28+
final jsonString = _prefs.getString(_cacheKey(key));
29+
if (jsonString == null) return null;
30+
31+
final timestamp = _prefs.getInt(_timestampKey(key));
32+
if (timestamp == null) return null;
33+
34+
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
35+
if (DateTime.now().difference(cacheTime) > cacheExpiry) {
36+
// Cache expired
37+
removeCache(key);
38+
return null;
39+
}
40+
41+
try {
42+
return jsonDecode(jsonString);
43+
} catch (_) {
44+
removeCache(key);
45+
return null;
46+
}
47+
}
48+
49+
Future<void> removeCache(String key) async {
50+
await _prefs.remove(_cacheKey(key));
51+
await _prefs.remove(_timestampKey(key));
52+
}
53+
}

macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import FlutterMacOS
66
import Foundation
77

8+
import connectivity_plus
89
import flutter_secure_storage_macos
910
import package_info_plus
1011
import path_provider_foundation
1112
import shared_preferences_foundation
1213
import url_launcher_macos
1314

1415
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
16+
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
1517
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
1618
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
1719
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

pubspec.lock

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Generated by pub
22
# See https://dart.dev/tools/pub/glossary#lockfile
33
packages:
4+
args:
5+
dependency: transitive
6+
description:
7+
name: args
8+
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
9+
url: "https://pub.dev"
10+
source: hosted
11+
version: "2.7.0"
412
async:
513
dependency: transitive
614
description:
@@ -57,6 +65,22 @@ packages:
5765
url: "https://pub.dev"
5866
source: hosted
5967
version: "1.19.1"
68+
connectivity_plus:
69+
dependency: "direct main"
70+
description:
71+
name: connectivity_plus
72+
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
73+
url: "https://pub.dev"
74+
source: hosted
75+
version: "4.0.2"
76+
connectivity_plus_platform_interface:
77+
dependency: transitive
78+
description:
79+
name: connectivity_plus_platform_interface
80+
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
81+
url: "https://pub.dev"
82+
source: hosted
83+
version: "1.2.4"
6084
cupertino_icons:
6185
dependency: "direct main"
6286
description:
@@ -65,6 +89,14 @@ packages:
6589
url: "https://pub.dev"
6690
source: hosted
6791
version: "1.0.8"
92+
dbus:
93+
dependency: transitive
94+
description:
95+
name: dbus
96+
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
97+
url: "https://pub.dev"
98+
source: hosted
99+
version: "0.7.11"
68100
dio:
69101
dependency: "direct main"
70102
description:
@@ -333,6 +365,14 @@ packages:
333365
url: "https://pub.dev"
334366
source: hosted
335367
version: "1.0.0"
368+
nm:
369+
dependency: transitive
370+
description:
371+
name: nm
372+
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
373+
url: "https://pub.dev"
374+
source: hosted
375+
version: "0.5.0"
336376
package_info_plus:
337377
dependency: "direct main"
338378
description:
@@ -369,10 +409,10 @@ packages:
369409
dependency: transitive
370410
description:
371411
name: path_provider_android
372-
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
412+
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
373413
url: "https://pub.dev"
374414
source: hosted
375-
version: "2.2.10"
415+
version: "2.2.17"
376416
path_provider_foundation:
377417
dependency: transitive
378418
description:
@@ -405,6 +445,14 @@ packages:
405445
url: "https://pub.dev"
406446
source: hosted
407447
version: "2.3.0"
448+
petitparser:
449+
dependency: transitive
450+
description:
451+
name: petitparser
452+
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
453+
url: "https://pub.dev"
454+
source: hosted
455+
version: "6.1.0"
408456
platform:
409457
dependency: transitive
410458
description:
@@ -606,10 +654,10 @@ packages:
606654
dependency: transitive
607655
description:
608656
name: url_launcher_web
609-
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
657+
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
610658
url: "https://pub.dev"
611659
source: hosted
612-
version: "2.4.0"
660+
version: "2.4.1"
613661
url_launcher_windows:
614662
dependency: transitive
615663
description:
@@ -646,10 +694,10 @@ packages:
646694
dependency: transitive
647695
description:
648696
name: win32
649-
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
697+
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
650698
url: "https://pub.dev"
651699
source: hosted
652-
version: "5.5.4"
700+
version: "5.12.0"
653701
xdg_directories:
654702
dependency: transitive
655703
description:
@@ -658,6 +706,14 @@ packages:
658706
url: "https://pub.dev"
659707
source: hosted
660708
version: "1.1.0"
709+
xml:
710+
dependency: transitive
711+
description:
712+
name: xml
713+
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
714+
url: "https://pub.dev"
715+
source: hosted
716+
version: "6.5.0"
661717
sdks:
662718
dart: ">=3.7.2 <4.0.0"
663719
flutter: ">=3.27.0"

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies:
2828
restart_app: ^1.2.1
2929
url_launcher: ^6.2.5
3030
package_info_plus: ^8.3.0
31+
connectivity_plus: ^4.0.1
3132

3233
dev_dependencies:
3334
flutter_test:

windows/flutter/generated_plugin_registrant.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
#include "generated_plugin_registrant.h"
88

9+
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
910
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
1011
#include <url_launcher_windows/url_launcher_windows.h>
1112

1213
void RegisterPlugins(flutter::PluginRegistry* registry) {
14+
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
15+
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
1316
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
1417
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
1518
UrlLauncherWindowsRegisterWithRegistrar(

windows/flutter/generated_plugins.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44

55
list(APPEND FLUTTER_PLUGIN_LIST
6+
connectivity_plus
67
flutter_secure_storage_windows
78
url_launcher_windows
89
)

0 commit comments

Comments
 (0)