Skip to content

Commit 52e12e0

Browse files
committed
chore: Support using cache when the network is unavailable (Dio)
1. open android (Android Studio Narwhal 2025.1.3) 2. open ios (XCode 26.x/17A400) 3. flutter (v3.35.4)
1 parent 3b3e8d8 commit 52e12e0

9 files changed

Lines changed: 348 additions & 119 deletions

File tree

android/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ plugins {
2020

2121
dependencies {
2222
// Import the Firebase BoM
23-
implementation(platform("com.google.firebase:firebase-bom:34.2.0"))
23+
implementation(platform("com.google.firebase:firebase-bom:34.3.0"))
2424

2525
// TODO: Add the dependencies for Firebase products you want to use
2626
// When using the BoM, don't specify versions in Firebase dependencies

ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ PODS:
2121
- FirebaseAuth (~> 12.2.0)
2222
- Firebase/CoreOnly (12.2.0):
2323
- FirebaseCore (~> 12.2.0)
24-
- firebase_auth (6.0.2):
24+
- firebase_auth (6.1.0):
2525
- Firebase/Auth (= 12.2.0)
2626
- firebase_core
2727
- Flutter
28-
- firebase_core (4.1.0):
28+
- firebase_core (4.1.1):
2929
- Firebase/CoreOnly (= 12.2.0)
3030
- Flutter
3131
- FirebaseAppCheckInterop (12.2.0)
@@ -203,8 +203,8 @@ SPEC CHECKSUMS:
203203
audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd
204204
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
205205
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
206-
firebase_auth: 9c0ce237c3bdc34c85fec8b87575beda143adba0
207-
firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302
206+
firebase_auth: 069b05a861a7c2f7a73112dd616a49a40f35ae52
207+
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
208208
FirebaseAppCheckInterop: a1b2598c64c5a8c42fd6f6a1c3d0938ae4324678
209209
FirebaseAuth: 059c11702bdb759bb49b6c7ec6ff67abf21f39c4
210210
FirebaseAuthInterop: 217702acd4cc6baa98ba9d6c054532e0de0b8a16

lib/api/auth.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ import 'package:dompet/configure/dio.dart';
22

33
class AuthApi {
44
static Future<void> login(Map<String, String> data) {
5-
final options = Options(extra: {'retry': 2});
6-
return fetch.post('/auth/login', data: data, options: options);
5+
return fetch.post(
6+
'/auth/login',
7+
options: Options(extra: {'retry': 2, 'cache': true}),
8+
queryParameters: null,
9+
data: data,
10+
);
711
}
812

913
static Future<void> register(Map<String, String> data) {
10-
return fetch.post('/auth/register', data: data);
14+
return fetch.post(
15+
'/auth/register',
16+
options: Options(extra: {'retry': 2}),
17+
queryParameters: null,
18+
data: data,
19+
);
1120
}
1221
}

lib/configure/dio.dart

Lines changed: 206 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,162 @@
1-
import 'package:get/get.dart';
1+
import 'dart:convert';
2+
import 'dart:typed_data';
23
import 'package:dio/dio.dart';
4+
export 'package:dio/dio.dart';
5+
import 'package:crypto/crypto.dart';
6+
import 'package:encrypt/encrypt.dart';
7+
import 'package:get/get.dart' hide Response;
38
import 'package:get_storage/get_storage.dart';
49
import 'package:dompet/configure/fluttertoast.dart';
10+
import 'package:dompet/extension/bool.dart';
511
import 'package:dompet/logger/printer.dart';
612
import 'package:dompet/logger/logger.dart';
713

8-
export 'package:dio/dio.dart';
9-
1014
final fetch = DioManager.fetch;
1115

1216
class DioManager {
17+
static String encrypt(String data, [String? key]) {
18+
final code = key.bv ? key! : 'XDompetTH90JK373';
19+
final encrypter = Encrypter(AES(Key.fromUtf8(code)));
20+
final encrypted = encrypter.encrypt(data, iv: IV(utf8.encode(code)));
21+
return encrypted.base64;
22+
}
23+
24+
static String decrypt(String data, [String? key]) {
25+
final code = key.bv ? key! : 'XDompetTH90JK373';
26+
final encrypter = Encrypter(AES(Key.fromUtf8(code)));
27+
final decrypted = encrypter.decrypt64(data, iv: IV(utf8.encode(code)));
28+
return decrypted;
29+
}
30+
31+
static String encode(dynamic data, [String? type]) {
32+
if (type == null && data is Uint8List) {
33+
return base64Encode(data);
34+
}
35+
36+
if (type == 'base64Encode') {
37+
return base64Encode(data);
38+
}
39+
40+
return jsonEncode(data);
41+
}
42+
43+
static dynamic decode(dynamic data, [String? type]) {
44+
if (type == 'base64Decode') {
45+
return base64Decode(data);
46+
}
47+
48+
return jsonDecode(data);
49+
}
50+
51+
static dynamic recorder(DioException err, [Response? res]) {
52+
err.requestOptions.extra.remove('code');
53+
err.requestOptions.extra.remove('cache');
54+
err.requestOptions.extra.remove('retry');
55+
err.requestOptions.extra.remove('logger');
56+
57+
logger.error(
58+
{
59+
'dio': 'network request failed',
60+
'path': err.requestOptions.uri,
61+
'method': err.requestOptions.method,
62+
'headers': err.requestOptions.headers,
63+
'bodyData': err.requestOptions.data,
64+
'queryExtra': err.requestOptions.extra,
65+
'queryParams': err.requestOptions.queryParameters,
66+
'responseData': err.response?.data,
67+
'responseExtra': err.response?.extra,
68+
'responseError': err.message ?? err.error,
69+
'responseUsage': res != null ? 'Cached' : 'Unknown',
70+
},
71+
err.error,
72+
err.stackTrace,
73+
);
74+
}
75+
76+
static Future<Response?> query(DioException err) async {
77+
try {
78+
final uri = err.requestOptions.uri.toString();
79+
final code = err.requestOptions.extra['code'] ?? uri;
80+
final cache = err.requestOptions.extra['cache'] ?? false;
81+
final unique = md5.convert(utf8.encode(code ?? uri)).toString();
82+
final source = cache ? GetStorage().read('dio_cache_$unique') : null;
83+
final storage = cache ? jsonDecode(decrypt(source)) : null;
84+
85+
if (cache == true && storage != null) {
86+
final options = storage['requestOptions'];
87+
88+
final headers = storage['headers'].map((k, v) {
89+
return MapEntry('$k', List<String>.from(v));
90+
});
91+
92+
final response = Response(
93+
requestOptions: RequestOptions(
94+
path: options['path'],
95+
extra: Map.from(options['extra']),
96+
method: options['method'] ?? 'GET',
97+
queryParameters: Map.from(options['queryParameters']),
98+
responseType: ResponseType.values[options['responseType']],
99+
contentType: options['contentType'],
100+
),
101+
data: DioManager.decode(storage['data'], storage['decode']),
102+
extra: Map.from(storage['extra'])..addAll({'cached': 'local'}),
103+
headers: Headers.fromMap(Map<String, List<String>>.from(headers)),
104+
statusMessage: storage['statusMessage'],
105+
statusCode: storage['statusCode'],
106+
);
107+
108+
return response;
109+
}
110+
} catch (e) {
111+
/* e */
112+
}
113+
114+
return null;
115+
}
116+
117+
static Future<Response> cache(Response res) async {
118+
if (res.data is ResponseBody) {
119+
return res;
120+
}
121+
122+
try {
123+
final uri = res.requestOptions.uri.toString();
124+
final code = res.requestOptions.extra['code'] ?? uri;
125+
final cache = res.requestOptions.extra['cache'] ?? false;
126+
final unique = md5.convert(utf8.encode(code ?? uri)).toString();
127+
128+
res.requestOptions.extra.remove('logger');
129+
res.requestOptions.extra.remove('retry');
130+
res.requestOptions.extra.remove('cache');
131+
res.requestOptions.extra.remove('code');
132+
133+
if (cache == true && unique.isNotEmpty) {
134+
final encode = jsonEncode({
135+
"requestOptions": {
136+
"path": res.requestOptions.path,
137+
"extra": res.requestOptions.extra,
138+
"method": res.requestOptions.method,
139+
"contentType": res.requestOptions.contentType,
140+
"responseType": res.requestOptions.responseType.index,
141+
"queryParameters": res.requestOptions.queryParameters,
142+
},
143+
"data": DioManager.encode(res.data),
144+
"decode": res.data is Uint8List ? 'base64Decode' : null,
145+
"statusMessage": res.statusMessage,
146+
"statusCode": res.statusCode,
147+
"headers": res.headers.map,
148+
"extra": res.extra,
149+
});
150+
151+
await GetStorage().write('dio_cache_$unique', encrypt(encode));
152+
}
153+
} catch (e) {
154+
/* e */
155+
}
156+
157+
return res;
158+
}
159+
13160
static Dio create(BaseOptions option) {
14161
final dio = Dio(
15162
BaseOptions(
@@ -34,49 +181,76 @@ class DioManager {
34181

35182
dio.interceptors.add(
36183
InterceptorsWrapper(
37-
onRequest: (options, handler) {
184+
onRequest: (options, handler) async {
38185
options.headers['token'] ??= GetStorage().read('token');
39-
options.extra['request'] ??= dateAndTime();
186+
options.extra['requested'] ??= dateAndTime();
187+
options.extra['responsed'] = null;
188+
options.extra['cache'] ??= false;
189+
options.extra['retry'] ??= -1;
40190
return handler.next(options);
41191
},
42-
onResponse: (response, handler) {
43-
response.requestOptions.extra['response'] ??= dateAndTime();
44-
response.requestOptions.extra['request'] ??= dateAndTime();
45-
return handler.next(response);
192+
onResponse: (response, handler) async {
193+
response.requestOptions.extra['requested'] ??= dateAndTime();
194+
response.requestOptions.extra['responsed'] ??= dateAndTime();
195+
196+
if (response.statusCode != 200 || response.data is ResponseBody) {
197+
response.requestOptions.extra['cache'] = false;
198+
}
199+
200+
if (response.requestOptions.extra['cache'] == true) {
201+
await DioManager.cache(response);
202+
}
203+
204+
response.requestOptions.extra.remove('logger');
205+
response.requestOptions.extra.remove('retry');
206+
response.requestOptions.extra.remove('cache');
207+
response.requestOptions.extra.remove('code');
208+
209+
return handler.resolve(response);
46210
},
47-
onError: (error, handler) {
211+
),
212+
);
213+
214+
dio.interceptors.add(
215+
InterceptorsWrapper(
216+
onError: (error, handler) async {
217+
String? message;
218+
Response? response;
219+
48220
switch (error.type) {
49221
case DioExceptionType.sendTimeout:
50222
case DioExceptionType.receiveTimeout:
223+
case DioExceptionType.connectionError:
51224
case DioExceptionType.connectionTimeout:
52-
if (error.requestOptions.extra['retry'] < 1) {
53-
Toaster.error(message: 'System_Network_Timeout'.tr);
225+
if (error.requestOptions.extra['retry'] >= 1) {
226+
return handler.next(error);
54227
}
55228

229+
message = 'System_Network_Timeout';
56230
break;
57231

58232
default:
59-
Toaster.error(message: 'System_Abnormality'.tr);
233+
message = 'System_Abnormality';
234+
break;
235+
}
236+
237+
error.requestOptions.extra['requested'] ??= dateAndTime();
238+
error.requestOptions.extra['responsed'] ??= dateAndTime();
239+
240+
if (error.requestOptions.extra['cache'] == true) {
241+
response = await DioManager.query(error);
60242
}
61243

62244
if (error.requestOptions.extra['logger'] == true) {
63-
error.requestOptions.extra['response'] ??= dateAndTime();
64-
error.requestOptions.extra['request'] ??= dateAndTime();
65-
66-
final message = {
67-
'dio': 'network request failed',
68-
'path': '${error.requestOptions.uri}',
69-
'method': error.requestOptions.method,
70-
'headers': '${error.requestOptions.headers}',
71-
'bodyData': '${error.requestOptions.data ?? ''}',
72-
'queryParams': '${error.requestOptions.queryParameters}',
73-
'requestTime': '${error.requestOptions.extra['request']}',
74-
'responseTime': '${error.requestOptions.extra['response']}',
75-
'responseData': '${error.response?.data ?? ''}',
76-
'responseError': '${error.message ?? error.error ?? ''}',
77-
};
78-
79-
logger.error(message, error.error, error.stackTrace);
245+
recorder(error, response);
246+
}
247+
248+
if (response is! Response) {
249+
Toaster.error(message: message.tr);
250+
}
251+
252+
if (response is Response) {
253+
return handler.resolve(response);
80254
}
81255

82256
return handler.next(error);
@@ -92,10 +266,9 @@ class DioManager {
92266
switch (error.type) {
93267
case DioExceptionType.sendTimeout:
94268
case DioExceptionType.receiveTimeout:
269+
case DioExceptionType.connectionError:
95270
case DioExceptionType.connectionTimeout:
96271
error.requestOptions.extra['retry'] -= 1;
97-
error.requestOptions.extra['request'] = null;
98-
error.requestOptions.extra['response'] = null;
99272
await Future.delayed(Duration(milliseconds: 100));
100273
return dio.fetch(error.requestOptions).then(handler.resolve);
101274

@@ -121,7 +294,7 @@ class DioManager {
121294
sendTimeout: const Duration(milliseconds: 3000),
122295
connectTimeout: const Duration(milliseconds: 3000),
123296
receiveTimeout: const Duration(milliseconds: 3000),
124-
extra: {'logger': true, 'retry': 0},
297+
extra: {'logger': true, 'cache': false, 'retry': -1},
125298
),
126299
);
127300
}

lib/service/bind.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ final bindings = [
2727
Bind.put<EventController>(EventController()),
2828
Bind.put<LocaleController>(LocaleController()),
2929
Bind.put<SqliteController>(SqliteController()),
30-
Bind.put<SocketController>(SocketController()),
3130
Bind.put<NetworkController>(NetworkController()),
3231
Bind.put<AppLinkController>(AppLinkController()),
32+
Bind.put<IOSocketController>(IOSocketController()),
3333
Bind.put<MediaQueryController>(MediaQueryController()),
3434
Bind.put<NativeChannelController>(NativeChannelController()),
3535
Bind.put<WebviewChannelController>(WebviewChannelController()),

0 commit comments

Comments
 (0)