feat(app): смена сервера API и очистка данных устройства при выходе

Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
This commit is contained in:
Dmitry
2026-09-19 22:14:14 +03:00
parent bfe74ca47c
commit a559d6de3e
17 changed files with 887 additions and 135 deletions
+12 -4
View File
@@ -28,7 +28,10 @@ class CacheInterceptor extends Interceptor {
};
@override
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) {
if (response.requestOptions.method == 'GET' && response.statusCode == 200) {
// Never block the UI on the write: the caller already has its data.
unawaited(_db.put(requestKey(response.requestOptions), response.data));
@@ -37,8 +40,12 @@ class CacheInterceptor extends Interceptor {
}
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.requestOptions.method != 'GET' || !_offlineTypes.contains(err.type)) {
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
if (err.requestOptions.method != 'GET' ||
!_offlineTypes.contains(err.type)) {
return handler.next(err);
}
final cached = await _db.get(requestKey(err.requestOptions));
@@ -56,7 +63,8 @@ class CacheInterceptor extends Interceptor {
/// `path?sorted=query` — deliberately excludes the host, so switching the API
/// origin (dev vs. prod) does not orphan a device's cache.
static String requestKey(RequestOptions options) {
final params = options.queryParameters.entries.toList()..sort((a, b) => a.key.compareTo(b.key));
final params = options.queryParameters.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final query = params.map((e) => '${e.key}=${e.value}').join('&');
return '${options.path}?$query';
}
+2 -1
View File
@@ -15,7 +15,8 @@ extension CachedResponseX<T> on Response<T> {
/// Wraps this response's data with the staleness [CacheInterceptor] recorded
/// in `extra['fetchedAt']`. A provider that wants offline support returns
/// `r.cached` instead of `r.data!`.
Cached<T> get cached => Cached(data as T, fetchedAt: extra['fetchedAt'] as DateTime?);
Cached<T> get cached =>
Cached(data as T, fetchedAt: extra['fetchedAt'] as DateTime?);
}
/// The oldest of several fetch times, or null if none of them is stale.
+11 -7
View File
@@ -38,7 +38,8 @@ class ResponseCacheDatabase extends _$ResponseCacheDatabase {
/// Replaces whatever was cached for [requestKey] — one row per endpoint+params,
/// never a history.
Future<void> put(String requestKey, Object? body) => into(cachedResponses).insertOnConflictUpdate(
Future<void> put(String requestKey, Object? body) =>
into(cachedResponses).insertOnConflictUpdate(
CachedResponsesCompanion.insert(
requestKey: requestKey,
body: jsonEncode(body),
@@ -46,6 +47,9 @@ class ResponseCacheDatabase extends _$ResponseCacheDatabase {
),
);
/// Drops every cached body — on sign-out, so the next account (or nobody) never sees them.
Future<void> clear() => delete(cachedResponses).go();
/// The body last stored for [requestKey], or null if this endpoint+params was
/// never fetched successfully on this device.
Future<CacheEntry?> get(String requestKey) async {
@@ -63,9 +67,9 @@ class ResponseCacheDatabase extends _$ResponseCacheDatabase {
/// the compiled assets it loads there (regenerate with `just app-web-assets`
/// whenever the `drift`/`sqlite3` package versions change).
QueryExecutor _openConnection() => driftDatabase(
name: 'response_cache',
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
name: 'response_cache',
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
+11
View File
@@ -0,0 +1,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'response_cache.dart';
/// One sqlite-backed cache of the last successful GET per endpoint+params,
/// shared by every screen. See `docs/ai/offline-cache.md`.
final responseCacheDbProvider = Provider<ResponseCacheDatabase>((ref) {
final db = ResponseCacheDatabase();
ref.onDispose(db.close);
return db;
});