init
This commit is contained in:
126
lib/core/network/api_client.dart
Normal file
126
lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_template/core/network/api_exception.dart';
|
||||
import 'package:flutter_template/core/network/api_response.dart';
|
||||
import 'package:flutter_template/core/network/http_method.dart';
|
||||
|
||||
typedef JsonParser<T> = T Function(dynamic json);
|
||||
|
||||
class ApiClient {
|
||||
const ApiClient(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
Future<T> request<T>(
|
||||
String path, {
|
||||
HttpMethod method = HttpMethod.get,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Object? data,
|
||||
Map<String, dynamic>? headers,
|
||||
JsonParser<T>? parser,
|
||||
bool wrapResponse = true,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.request<dynamic>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
data: data,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
options: Options(method: method.value, headers: headers),
|
||||
);
|
||||
|
||||
final raw = _decodeIfNeeded(response.data);
|
||||
if (!wrapResponse) {
|
||||
return _parseData<T>(raw, parser);
|
||||
}
|
||||
|
||||
if (raw is! Map<String, dynamic>) {
|
||||
return _parseData<T>(raw, parser);
|
||||
}
|
||||
|
||||
final wrapped = ApiResponse<T>.fromJson(raw, fromJsonT: parser);
|
||||
if (!wrapped.isSuccess) {
|
||||
throw ApiException(
|
||||
code: wrapped.code,
|
||||
statusCode: response.statusCode,
|
||||
message: wrapped.message.isEmpty ? '请求失败' : wrapped.message,
|
||||
details: raw,
|
||||
);
|
||||
}
|
||||
|
||||
return wrapped.data as T;
|
||||
} on DioException catch (error) {
|
||||
throw _mapDioException(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
JsonParser<T>? parser,
|
||||
bool wrapResponse = true,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: HttpMethod.get,
|
||||
queryParameters: queryParameters,
|
||||
parser: parser,
|
||||
wrapResponse: wrapResponse,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> post<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
JsonParser<T>? parser,
|
||||
bool wrapResponse = true,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: HttpMethod.post,
|
||||
data: data,
|
||||
parser: parser,
|
||||
wrapResponse: wrapResponse,
|
||||
);
|
||||
}
|
||||
|
||||
dynamic _decodeIfNeeded(dynamic data) {
|
||||
if (data is String) {
|
||||
try {
|
||||
return jsonDecode(data);
|
||||
} catch (_) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
T _parseData<T>(dynamic data, JsonParser<T>? parser) {
|
||||
if (parser != null) return parser(data);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
ApiException _mapDioException(DioException error) {
|
||||
final statusCode = error.response?.statusCode;
|
||||
final message = switch (error.type) {
|
||||
DioExceptionType.connectionTimeout => '网络连接超时',
|
||||
DioExceptionType.sendTimeout => '请求发送超时',
|
||||
DioExceptionType.receiveTimeout => '响应接收超时',
|
||||
DioExceptionType.badCertificate => '证书校验失败',
|
||||
DioExceptionType.badResponse => '服务器响应异常',
|
||||
DioExceptionType.cancel => '请求已取消',
|
||||
DioExceptionType.connectionError => '网络连接不可用',
|
||||
DioExceptionType.unknown => '网络请求失败',
|
||||
};
|
||||
|
||||
return ApiException(
|
||||
message: message,
|
||||
statusCode: statusCode,
|
||||
details: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/core/network/api_exception.dart
Normal file
23
lib/core/network/api_exception.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
class ApiException implements Exception {
|
||||
const ApiException({
|
||||
required this.message,
|
||||
this.code,
|
||||
this.statusCode,
|
||||
this.details,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final int? code;
|
||||
final int? statusCode;
|
||||
final Object? details;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final parts = <String>[
|
||||
if (statusCode != null) 'status=$statusCode',
|
||||
if (code != null) 'code=$code',
|
||||
message,
|
||||
];
|
||||
return 'ApiException(${parts.join(', ')})';
|
||||
}
|
||||
}
|
||||
49
lib/core/network/api_response.dart
Normal file
49
lib/core/network/api_response.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
class ApiResponse<T> {
|
||||
const ApiResponse({required this.code, required this.message, this.data});
|
||||
|
||||
final int code;
|
||||
final String message;
|
||||
final T? data;
|
||||
|
||||
bool get isSuccess => code >= 200 && code < 300;
|
||||
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
}) {
|
||||
return ApiResponse<T>(
|
||||
code: (json['code'] as num?)?.toInt() ?? 200,
|
||||
message: (json['message'] ?? json['msg'] ?? '').toString(),
|
||||
data: fromJsonT == null ? json['data'] as T? : fromJsonT(json['data']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageResponse<T> {
|
||||
const PageResponse({
|
||||
required this.items,
|
||||
required this.total,
|
||||
this.page,
|
||||
this.pageSize,
|
||||
});
|
||||
|
||||
final List<T> items;
|
||||
final int total;
|
||||
final int? page;
|
||||
final int? pageSize;
|
||||
|
||||
bool get isEmpty => items.isEmpty;
|
||||
|
||||
factory PageResponse.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
required T Function(dynamic json) fromJsonT,
|
||||
}) {
|
||||
final rawItems = json['items'] ?? json['rows'] ?? json['list'] ?? const [];
|
||||
return PageResponse<T>(
|
||||
items: rawItems is List ? rawItems.map(fromJsonT).toList() : <T>[],
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
page: (json['page'] as num?)?.toInt(),
|
||||
pageSize: (json['pageSize'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/core/network/header_interceptor.dart
Normal file
31
lib/core/network/header_interceptor.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_template/app/config/app_config.dart';
|
||||
import 'package:flutter_template/core/cache/app_storage.dart';
|
||||
import 'package:flutter_template/core/cache/storage_keys.dart';
|
||||
import 'package:flutter_template/core/utils/device_utils.dart';
|
||||
|
||||
class HeaderInterceptor extends Interceptor {
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
final token = AppStorage.getString(StorageKeys.authToken);
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
final packageInfo = AppConfig.packageInfo;
|
||||
final device = await DeviceUtils.deviceInfo();
|
||||
options.headers.addAll({
|
||||
'platform': Platform.operatingSystem,
|
||||
if (packageInfo != null) 'appVersion': packageInfo.version,
|
||||
'environment': AppConfig.current.environment.name,
|
||||
...device,
|
||||
});
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
11
lib/core/network/http_method.dart
Normal file
11
lib/core/network/http_method.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
enum HttpMethod {
|
||||
get('GET'),
|
||||
post('POST'),
|
||||
put('PUT'),
|
||||
patch('PATCH'),
|
||||
delete('DELETE');
|
||||
|
||||
const HttpMethod(this.value);
|
||||
|
||||
final String value;
|
||||
}
|
||||
58
lib/core/network/network_monitor.dart
Normal file
58
lib/core/network/network_monitor.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_template/core/network/network_state.dart';
|
||||
|
||||
class NetworkMonitor {
|
||||
final _controller = StreamController<NetworkState>.broadcast();
|
||||
final _connectivity = Connectivity();
|
||||
|
||||
StreamSubscription<List<ConnectivityResult>>? _subscription;
|
||||
NetworkState _state = const NetworkState(Reachability.unknown);
|
||||
|
||||
Stream<NetworkState> get stream => _controller.stream;
|
||||
NetworkState get current => _state;
|
||||
|
||||
void start() {
|
||||
try {
|
||||
_subscription = _connectivity.onConnectivityChanged.listen(
|
||||
(results) => _setOnline(_isOnline(results)),
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
debugPrint('[NetworkMonitor] connectivity error: $error');
|
||||
},
|
||||
);
|
||||
|
||||
_connectivity.checkConnectivity().then(
|
||||
(results) => _setOnline(_isOnline(results)),
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
debugPrint('[NetworkMonitor] check error: $error');
|
||||
},
|
||||
);
|
||||
} on MissingPluginException catch (error) {
|
||||
debugPrint('[NetworkMonitor] plugin unavailable: $error');
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOnline(List<ConnectivityResult> results) {
|
||||
if (results.isEmpty) return false;
|
||||
if (results.contains(ConnectivityResult.none)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _setOnline(bool online) {
|
||||
final next = NetworkState(
|
||||
online ? Reachability.online : Reachability.offline,
|
||||
);
|
||||
if (next.reachability == _state.reachability) return;
|
||||
|
||||
_state = next;
|
||||
_controller.add(_state);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
11
lib/core/network/network_state.dart
Normal file
11
lib/core/network/network_state.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
enum Reachability { unknown, online, offline }
|
||||
|
||||
class NetworkState {
|
||||
const NetworkState(this.reachability);
|
||||
|
||||
final Reachability reachability;
|
||||
|
||||
bool get isOnline => reachability == Reachability.online;
|
||||
bool get isOffline => reachability == Reachability.offline;
|
||||
bool get isUnknown => reachability == Reachability.unknown;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class OfflineQueueInterceptor extends Interceptor {
|
||||
OfflineQueueInterceptor({
|
||||
required this.monitor,
|
||||
required this.manager,
|
||||
this.enabled = false,
|
||||
});
|
||||
|
||||
final NetworkMonitor monitor;
|
||||
final OfflineQueueManager manager;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (!enabled || options.extra['offline'] != true) {
|
||||
handler.next(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.extra['replay'] == true || monitor.current.isUnknown) {
|
||||
handler.next(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.current.isOffline) {
|
||||
final extra = options.extra;
|
||||
final request = OfflineRequest(
|
||||
id: const Uuid().v4(),
|
||||
idempotencyKey: (extra['idempotencyKey'] ?? const Uuid().v4())
|
||||
.toString(),
|
||||
method: options.method,
|
||||
path: options.path,
|
||||
query: options.queryParameters,
|
||||
body: options.data,
|
||||
headers: Map<String, dynamic>.from(options.headers),
|
||||
priority: QueuePriority
|
||||
.values[(extra['priority'] as int?) ?? QueuePriority.normal.index],
|
||||
category:
|
||||
QueueCategory.values[(extra['category'] as int?) ??
|
||||
QueueCategory.userAction.index],
|
||||
);
|
||||
|
||||
manager.enqueue(request);
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: options,
|
||||
type: DioExceptionType.cancel,
|
||||
message: 'OFFLINE_QUEUED',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
141
lib/core/network/offline_queue/offline_queue_manager.dart
Normal file
141
lib/core/network/offline_queue/offline_queue_manager.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_template/core/logging/app_logger.dart';
|
||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
|
||||
|
||||
class OfflineQueueManager {
|
||||
OfflineQueueManager({
|
||||
required this.dio,
|
||||
required this.monitor,
|
||||
required this.storage,
|
||||
});
|
||||
|
||||
final Dio dio;
|
||||
final NetworkMonitor monitor;
|
||||
final OfflineQueueStorage storage;
|
||||
|
||||
final _queue = <OfflineRequest>[];
|
||||
final _dead = <OfflineRequest>[];
|
||||
final _stateController = StreamController<OfflineQueueState>.broadcast();
|
||||
|
||||
StreamSubscription<dynamic>? _networkSubscription;
|
||||
Timer? _replayDebounceTimer;
|
||||
bool _replaying = false;
|
||||
|
||||
Stream<OfflineQueueState> get stateStream => _stateController.stream;
|
||||
|
||||
Future<void> init() async {
|
||||
_queue.addAll(await storage.loadQueue());
|
||||
_dead.addAll(await storage.loadDead());
|
||||
_sort();
|
||||
_emit();
|
||||
|
||||
_networkSubscription = monitor.stream.listen((state) {
|
||||
if (state.isOffline) {
|
||||
_replayDebounceTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isOnline) {
|
||||
_replayDebounceTimer?.cancel();
|
||||
_replayDebounceTimer = Timer(const Duration(seconds: 2), replay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> enqueue(OfflineRequest request) async {
|
||||
if (_isDuplicate(request)) return;
|
||||
|
||||
_queue.add(request);
|
||||
_sort();
|
||||
await storage.saveQueue(_queue);
|
||||
_emit();
|
||||
}
|
||||
|
||||
Future<void> replay() async {
|
||||
if (!monitor.current.isOnline || _replaying || _queue.isEmpty) return;
|
||||
|
||||
_replaying = true;
|
||||
_emit();
|
||||
|
||||
try {
|
||||
while (_queue.isNotEmpty && monitor.current.isOnline) {
|
||||
final request = _queue.first;
|
||||
try {
|
||||
await dio.request<dynamic>(
|
||||
request.path,
|
||||
data: request.body,
|
||||
queryParameters: request.query,
|
||||
options: Options(
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
extra: const {'replay': true, 'offline': false},
|
||||
),
|
||||
);
|
||||
_queue.removeAt(0);
|
||||
await storage.saveQueue(_queue);
|
||||
_emit();
|
||||
} catch (error, stackTrace) {
|
||||
AppLogger.debug(
|
||||
'Offline replay failed: ${request.method} ${request.path}',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
final updated = request.copyWith(retryCount: request.retryCount + 1);
|
||||
_queue.removeAt(0);
|
||||
if (updated.isDead) {
|
||||
_dead.add(updated);
|
||||
await storage.saveDead(_dead);
|
||||
} else {
|
||||
_queue.add(updated);
|
||||
_sort();
|
||||
}
|
||||
await storage.saveQueue(_queue);
|
||||
_emit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_replaying = false;
|
||||
_emit();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDuplicate(OfflineRequest request) {
|
||||
return _queue.any(
|
||||
(item) =>
|
||||
(request.idempotencyKey.isNotEmpty &&
|
||||
item.idempotencyKey == request.idempotencyKey) ||
|
||||
(item.method == request.method && item.path == request.path),
|
||||
);
|
||||
}
|
||||
|
||||
void _sort() {
|
||||
_queue.sort((a, b) {
|
||||
final priority = a.priority.index.compareTo(b.priority.index);
|
||||
return priority != 0
|
||||
? priority
|
||||
: a.category.index.compareTo(b.category.index);
|
||||
});
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
_stateController.add(
|
||||
OfflineQueueState(
|
||||
pending: _queue.length,
|
||||
dead: _dead.length,
|
||||
replaying: _replaying,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_replayDebounceTimer?.cancel();
|
||||
_networkSubscription?.cancel();
|
||||
_stateController.close();
|
||||
}
|
||||
}
|
||||
13
lib/core/network/offline_queue/offline_queue_state.dart
Normal file
13
lib/core/network/offline_queue/offline_queue_state.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class OfflineQueueState {
|
||||
const OfflineQueueState({
|
||||
required this.pending,
|
||||
required this.dead,
|
||||
required this.replaying,
|
||||
});
|
||||
|
||||
final int pending;
|
||||
final int dead;
|
||||
final bool replaying;
|
||||
|
||||
static const empty = OfflineQueueState(pending: 0, dead: 0, replaying: false);
|
||||
}
|
||||
29
lib/core/network/offline_queue/offline_queue_storage.dart
Normal file
29
lib/core/network/offline_queue/offline_queue_storage.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter_template/core/cache/app_storage.dart';
|
||||
import 'package:flutter_template/core/cache/storage_keys.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
|
||||
|
||||
class OfflineQueueStorage {
|
||||
Future<List<OfflineRequest>> loadQueue() async {
|
||||
final list = AppStorage.getStringList(StorageKeys.offlineQueue);
|
||||
return list.map(OfflineRequest.decode).toList();
|
||||
}
|
||||
|
||||
Future<List<OfflineRequest>> loadDead() async {
|
||||
final list = AppStorage.getStringList(StorageKeys.offlineDeadQueue);
|
||||
return list.map(OfflineRequest.decode).toList();
|
||||
}
|
||||
|
||||
Future<void> saveQueue(List<OfflineRequest> list) async {
|
||||
await AppStorage.setStringList(
|
||||
StorageKeys.offlineQueue,
|
||||
list.map((item) => item.encode()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveDead(List<OfflineRequest> list) async {
|
||||
await AppStorage.setStringList(
|
||||
StorageKeys.offlineDeadQueue,
|
||||
list.map((item) => item.encode()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/core/network/offline_queue/offline_request.dart
Normal file
89
lib/core/network/offline_queue/offline_request.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'dart:convert';
|
||||
|
||||
enum QueuePriority { high, normal, low }
|
||||
|
||||
enum QueueCategory { userAction, sync, analytics, log }
|
||||
|
||||
class OfflineRequest {
|
||||
const OfflineRequest({
|
||||
required this.id,
|
||||
required this.idempotencyKey,
|
||||
required this.method,
|
||||
required this.path,
|
||||
this.query,
|
||||
this.body,
|
||||
this.headers,
|
||||
this.priority = QueuePriority.normal,
|
||||
this.category = QueueCategory.sync,
|
||||
this.retryCount = 0,
|
||||
this.maxRetry = 3,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String idempotencyKey;
|
||||
final String method;
|
||||
final String path;
|
||||
final Map<String, dynamic>? query;
|
||||
final dynamic body;
|
||||
final Map<String, dynamic>? headers;
|
||||
final QueuePriority priority;
|
||||
final QueueCategory category;
|
||||
final int retryCount;
|
||||
final int maxRetry;
|
||||
|
||||
bool get isDead => retryCount >= maxRetry;
|
||||
|
||||
OfflineRequest copyWith({int? retryCount}) {
|
||||
return OfflineRequest(
|
||||
id: id,
|
||||
idempotencyKey: idempotencyKey,
|
||||
method: method,
|
||||
path: path,
|
||||
query: query,
|
||||
body: body,
|
||||
headers: headers,
|
||||
priority: priority,
|
||||
category: category,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
maxRetry: maxRetry,
|
||||
);
|
||||
}
|
||||
|
||||
factory OfflineRequest.fromJson(Map<String, dynamic> json) {
|
||||
return OfflineRequest(
|
||||
id: json['id'] as String,
|
||||
idempotencyKey: json['idempotencyKey'] as String,
|
||||
method: json['method'] as String,
|
||||
path: json['path'] as String,
|
||||
query: (json['query'] as Map?)?.cast<String, dynamic>(),
|
||||
body: json['body'],
|
||||
headers: (json['headers'] as Map?)?.cast<String, dynamic>(),
|
||||
priority: QueuePriority.values[(json['priority'] as num).toInt()],
|
||||
category: QueueCategory.values[(json['category'] as num).toInt()],
|
||||
retryCount: (json['retryCount'] as num).toInt(),
|
||||
maxRetry: (json['maxRetry'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'idempotencyKey': idempotencyKey,
|
||||
'method': method,
|
||||
'path': path,
|
||||
'query': query,
|
||||
'body': body,
|
||||
'headers': headers,
|
||||
'priority': priority.index,
|
||||
'category': category.index,
|
||||
'retryCount': retryCount,
|
||||
'maxRetry': maxRetry,
|
||||
};
|
||||
}
|
||||
|
||||
String encode() => jsonEncode(toJson());
|
||||
|
||||
static OfflineRequest decode(String raw) {
|
||||
return OfflineRequest.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
42
lib/core/network/providers/dio_providers.dart
Normal file
42
lib/core/network/providers/dio_providers.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_template/app/config/app_config.dart';
|
||||
import 'package:flutter_template/core/network/api_client.dart';
|
||||
import 'package:flutter_template/core/network/header_interceptor.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart';
|
||||
import 'package:flutter_template/core/network/providers/network_providers.dart';
|
||||
import 'package:flutter_template/core/network/providers/offline_queue_providers.dart';
|
||||
|
||||
final dioProvider = Provider<Dio>((ref) {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: AppConfig.current.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
sendTimeout: const Duration(seconds: 30),
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
dio.interceptors.add(HeaderInterceptor());
|
||||
|
||||
final monitor = ref.watch(networkMonitorProvider);
|
||||
final queueManager = ref.watch(offlineQueueManagerProvider);
|
||||
dio.interceptors.add(
|
||||
OfflineQueueInterceptor(
|
||||
monitor: monitor,
|
||||
manager: queueManager,
|
||||
enabled: false,
|
||||
),
|
||||
);
|
||||
|
||||
if (AppConfig.current.enableNetworkLog) {
|
||||
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
}
|
||||
|
||||
return dio;
|
||||
});
|
||||
|
||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||||
return ApiClient(ref.watch(dioProvider));
|
||||
});
|
||||
14
lib/core/network/providers/network_providers.dart
Normal file
14
lib/core/network/providers/network_providers.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_template/core/network/network_monitor.dart';
|
||||
import 'package:flutter_template/core/network/network_state.dart';
|
||||
|
||||
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||
final monitor = NetworkMonitor()..start();
|
||||
ref.onDispose(monitor.dispose);
|
||||
return monitor;
|
||||
});
|
||||
|
||||
final networkStateProvider = StreamProvider<NetworkState>((ref) {
|
||||
final monitor = ref.watch(networkMonitorProvider);
|
||||
return monitor.stream;
|
||||
});
|
||||
29
lib/core/network/providers/offline_queue_providers.dart
Normal file
29
lib/core/network/providers/offline_queue_providers.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
|
||||
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
|
||||
import 'package:flutter_template/core/network/providers/network_providers.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {
|
||||
return OfflineQueueStorage();
|
||||
});
|
||||
|
||||
final offlineQueueDioProvider = Provider<Dio>((ref) {
|
||||
return Dio();
|
||||
});
|
||||
|
||||
final offlineQueueManagerProvider = Provider<OfflineQueueManager>((ref) {
|
||||
final manager = OfflineQueueManager(
|
||||
dio: ref.watch(offlineQueueDioProvider),
|
||||
monitor: ref.watch(networkMonitorProvider),
|
||||
storage: ref.watch(offlineQueueStorageProvider),
|
||||
);
|
||||
manager.init();
|
||||
ref.onDispose(manager.dispose);
|
||||
return manager;
|
||||
});
|
||||
|
||||
final offlineQueueStateProvider = StreamProvider<OfflineQueueState>((ref) {
|
||||
return ref.watch(offlineQueueManagerProvider).stateStream;
|
||||
});
|
||||
Reference in New Issue
Block a user