Initial project
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
|
||||
class NetworksSdkMethodChannel
|
||||
{
|
||||
// Channel Name
|
||||
static const String channelName = 'networks_sdk';
|
||||
|
||||
//---------------- Flutter call native ----------------
|
||||
|
||||
static const String requestPermission = 'requestPermission';
|
||||
|
||||
|
||||
//---------------- Flutter call native ----------------
|
||||
|
||||
|
||||
|
||||
//---------------- native call Flutter ----------------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//---------------- native call Flutter ----------------
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/auth_interceptor.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/logging_interceptor.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/retry_interceptor.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
/// REST API 客户端
|
||||
/// 基于 Dio,提供 `executeRequest<T>` 唯一入口
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// final client = ApiClient(config: apiConfig);
|
||||
/// final data = await client.executeRequest(LoginRequest(...));
|
||||
/// ```
|
||||
class ApiClient {
|
||||
final ApiConfig config;
|
||||
final Dio _dio;
|
||||
|
||||
ApiClient({
|
||||
required this.config,
|
||||
Dio? dio,
|
||||
List<Interceptor>? additionalInterceptors,
|
||||
}) : _dio = dio ?? Dio() {
|
||||
// 配置默认选项
|
||||
_dio.options = BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
);
|
||||
|
||||
// 挂载拦截器(顺序:Auth → 自定义 → Retry → Logging)
|
||||
_dio.interceptors.addAll([
|
||||
AuthInterceptor(config),
|
||||
if (additionalInterceptors != null) ...additionalInterceptors,
|
||||
RetryInterceptor(config: config, dio: _dio),
|
||||
LoggingInterceptor(onLog: config.onLog),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 暴露 Dio 实例(供需要直接操作的场景,如文件上传)
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// DioException → ApiError 映射
|
||||
ApiError mapDioError(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
return const ApiError.timeout();
|
||||
case DioExceptionType.connectionError:
|
||||
return const ApiError.noNetworkConnection();
|
||||
default:
|
||||
if (e.response != null) {
|
||||
return ApiError.apiError(
|
||||
code: e.response!.statusCode ?? 0,
|
||||
message: e.response!.statusMessage ??
|
||||
e.message ??
|
||||
'Request failed',
|
||||
);
|
||||
}
|
||||
return ApiError.networkError(e.message ?? 'Network error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
|
||||
|
||||
/// 认证拦截器
|
||||
/// 自动注入 token 和默认 headers 到每个请求
|
||||
class AuthInterceptor extends Interceptor {
|
||||
final ApiConfig config;
|
||||
|
||||
AuthInterceptor(this.config);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
// 从 options.extra 读取请求元数据
|
||||
final requestType =
|
||||
options.extra['requestType'] as ApiRequestType? ?? ApiRequestType.request;
|
||||
final includeToken = options.extra['includeToken'] as bool? ?? true;
|
||||
final customHeaders =
|
||||
options.extra['customHeaders'] as Map<String, String>?;
|
||||
|
||||
// 保留重试请求的原始 Request-ID(幂等性)
|
||||
// 重试时 options.headers 中已有 APP-Request-ID,
|
||||
// 新生成的 headers 会覆盖它导致服务端无法识别为同一请求。
|
||||
final existingRequestId = options.headers['APP-Request-ID'] as String?;
|
||||
|
||||
// 构建 headers
|
||||
final headers = config.defaultHeaders(
|
||||
includeToken: includeToken && requestType != ApiRequestType.login,
|
||||
customHeaders: customHeaders,
|
||||
);
|
||||
|
||||
// 还原原始 Request-ID
|
||||
if (existingRequestId != null) {
|
||||
headers['APP-Request-ID'] = existingRequestId;
|
||||
}
|
||||
|
||||
options.headers.addAll(headers);
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
|
||||
/// 日志拦截器
|
||||
/// 统一打印请求 + 响应(一个日志块)
|
||||
class LoggingInterceptor extends Interceptor {
|
||||
final OnLog? onLog;
|
||||
final bool enabled;
|
||||
|
||||
LoggingInterceptor({this.onLog, this.enabled = true});
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (enabled && onLog != null) {
|
||||
_logRequestAndResponse(response);
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (enabled && onLog != null) {
|
||||
_logError(err);
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
void _logRequestAndResponse(Response response) {
|
||||
try {
|
||||
final logData = {
|
||||
'url': response.requestOptions.uri.toString(),
|
||||
'method': response.requestOptions.method,
|
||||
'request': {
|
||||
if (response.requestOptions.data != null)
|
||||
'body': response.requestOptions.data,
|
||||
},
|
||||
'response': {
|
||||
'status': response.statusCode,
|
||||
if (response.data != null) 'body': response.data,
|
||||
},
|
||||
};
|
||||
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
onLog!(
|
||||
'API Request + Response:\n${encoder.convert(logData)}',
|
||||
tag: 'Network',
|
||||
);
|
||||
} catch (e) {
|
||||
onLog!(
|
||||
'API: ${response.requestOptions.uri} -> ${response.statusCode}',
|
||||
tag: 'Network',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _logError(DioException error) {
|
||||
try {
|
||||
final logData = {
|
||||
'url': error.requestOptions.uri.toString(),
|
||||
'method': error.requestOptions.method,
|
||||
'type': error.type.toString(),
|
||||
'message': error.message,
|
||||
if (error.response != null) ...{
|
||||
'status': error.response!.statusCode,
|
||||
'data': error.response!.data,
|
||||
},
|
||||
};
|
||||
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
onLog!(
|
||||
'API Error:\n${encoder.convert(logData)}',
|
||||
tag: 'Network',
|
||||
);
|
||||
} catch (e) {
|
||||
onLog!('API Error: ${error.message}', tag: 'Network');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
|
||||
/// 重试拦截器
|
||||
///
|
||||
/// 两层重试机制:
|
||||
///
|
||||
/// 1. **Token 刷新重试**(onResponse)
|
||||
/// 检测 Token 过期响应 → 触发刷新回调 → 用新 Token 重试原请求
|
||||
///
|
||||
/// 2. **瞬态错误重试**(onError)
|
||||
/// 5xx / 超时 / 连接失败 → 指数退避 + jitter → 自动重试
|
||||
/// 由 [ApiConfig.maxRetries] 控制(默认 0 = 不启用)
|
||||
///
|
||||
/// 两层独立运作,可叠加。
|
||||
class RetryInterceptor extends Interceptor {
|
||||
final ApiConfig config;
|
||||
final Dio dio;
|
||||
|
||||
/// Token 刷新锁(防止多个请求同时刷新)
|
||||
bool _isRefreshing = false;
|
||||
Completer<bool>? _refreshCompleter;
|
||||
|
||||
final _random = Random();
|
||||
|
||||
RetryInterceptor({required this.config, required this.dio});
|
||||
|
||||
// ── Token 刷新重试 ────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final code = _parseCode(data['code']);
|
||||
|
||||
// 检查强制登出
|
||||
if (config.forceLogoutCodes.contains(code)) {
|
||||
config.onLog?.call(
|
||||
'Force logout detected (code: $code)',
|
||||
tag: 'Network',
|
||||
);
|
||||
config.onForceLogout?.call();
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Force logout (code: $code)',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 Token 过期
|
||||
if (config.tokenExpiredCodes.contains(code)) {
|
||||
config.onLog?.call(
|
||||
'Token expired (code: $code), refreshing...',
|
||||
tag: 'Network',
|
||||
);
|
||||
_handleTokenExpired(response, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
/// 处理 Token 过期:刷新 + 重试
|
||||
Future<void> _handleTokenExpired(
|
||||
Response response,
|
||||
ResponseInterceptorHandler handler,
|
||||
) async {
|
||||
final refreshSuccess = await _refreshToken();
|
||||
|
||||
if (!refreshSuccess) {
|
||||
config.onLog?.call('Token refresh failed', tag: 'Network');
|
||||
config.onForceLogout?.call();
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Token refresh failed',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 刷新成功,用新 token 重试原请求
|
||||
config.onLog?.call('Token refreshed, retrying...', tag: 'Network');
|
||||
try {
|
||||
final options = response.requestOptions;
|
||||
// 更新 header 中的 token
|
||||
options.headers['token'] = config.token;
|
||||
|
||||
final retryResponse = await dio.fetch(options);
|
||||
handler.resolve(retryResponse);
|
||||
} on DioException catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Token 刷新(串行锁)
|
||||
/// 多个请求同时过期时,只刷新一次,其余等待
|
||||
Future<bool> _refreshToken() async {
|
||||
if (_isRefreshing) {
|
||||
// 等待正在进行的刷新
|
||||
return _refreshCompleter?.future ?? Future.value(false);
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
_refreshCompleter = Completer<bool>();
|
||||
|
||||
try {
|
||||
if (config.onTokenRefresh == null) {
|
||||
_refreshCompleter!.complete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
final newToken = await config.onTokenRefresh!();
|
||||
final success = newToken != null;
|
||||
|
||||
if (success) {
|
||||
config.updateToken(newToken);
|
||||
}
|
||||
|
||||
_refreshCompleter!.complete(success);
|
||||
return success;
|
||||
} catch (e) {
|
||||
_refreshCompleter!.complete(false);
|
||||
return false;
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
_refreshCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 瞬态错误重试(指数退避 + jitter)────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (config.maxRetries <= 0 || !_isRetryable(err)) {
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
final options = err.requestOptions;
|
||||
final attempt = (options.extra['_retryAttempt'] as int?) ?? 0;
|
||||
|
||||
if (attempt >= config.maxRetries) {
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
options.extra['_retryAttempt'] = attempt + 1;
|
||||
|
||||
final delayMs = _backoffDelay(attempt);
|
||||
config.onLog?.call(
|
||||
'Transient error, retry ${attempt + 1}/${config.maxRetries} '
|
||||
'in ${delayMs}ms: ${options.path}',
|
||||
tag: 'Retry',
|
||||
);
|
||||
|
||||
await Future<void>.delayed(Duration(milliseconds: delayMs));
|
||||
|
||||
try {
|
||||
handler.resolve(await dio.fetch(options));
|
||||
} on DioException catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断是否可重试的瞬态错误
|
||||
bool _isRetryable(DioException err) {
|
||||
switch (err.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.connectionError:
|
||||
return true;
|
||||
case DioExceptionType.badResponse:
|
||||
// 5xx 服务端错误可重试
|
||||
final statusCode = err.response?.statusCode;
|
||||
return statusCode != null && statusCode >= 500;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 指数退避 + jitter
|
||||
///
|
||||
/// delay = min(baseDelay * 2^attempt, 30s) + random(0, delay * 25%)
|
||||
int _backoffDelay(int attempt) {
|
||||
final baseMs = config.retryBaseDelay.inMilliseconds;
|
||||
final exponentialMs = min(baseMs * pow(2, attempt).toInt(), 30000);
|
||||
final jitterMs = _random.nextInt((exponentialMs * 0.25).toInt().clamp(1, 7500));
|
||||
return exponentialMs + jitterMs;
|
||||
}
|
||||
|
||||
int _parseCode(dynamic code) {
|
||||
if (code is int) return code;
|
||||
if (code is String) return int.tryParse(code) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/api_client.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
import '../../../networks_sdk_platform_interface.dart';
|
||||
import '../../domain/entities/http_method.dart';
|
||||
|
||||
class NetworksSdkMethodChannelDataSource
|
||||
{
|
||||
final NetworksSdkPlatform platform;
|
||||
|
||||
late ApiClient apiClient;
|
||||
|
||||
NetworksSdkMethodChannelDataSource(this.platform);
|
||||
|
||||
Future<String?> getPlatformVersion() async {
|
||||
return await getPlatformVersion();
|
||||
}
|
||||
|
||||
void initialize(ApiConfig apiConfig){
|
||||
apiClient = ApiClient(config: apiConfig);
|
||||
}
|
||||
|
||||
/// 执行 API 请求 — 唯一入口
|
||||
///
|
||||
/// 流程:网络前置检查 → 构建 URL → 设置元数据 → 执行请求 → 解码响应 → 错误映射
|
||||
/// 拦截器负责:header 注入、Token 刷新重试、日志
|
||||
///
|
||||
/// Upload 类型支持两种模式:
|
||||
/// - 自有后端上传:path 为相对路径,自动拼接 baseURL
|
||||
/// - S3 presigned URL:path 以 http 开头,直接使用全路径
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) async {
|
||||
// 前置检查:网络不可用时直接抛错,避免无效请求
|
||||
if (apiClient.config.onCheckNetworkAvailable != null) {
|
||||
final available = await apiClient.config.onCheckNetworkAvailable!();
|
||||
if (!available) {
|
||||
apiClient.config.onLog?.call(
|
||||
'Network unavailable, abort request: ${request.path}',
|
||||
tag: 'ApiClient',
|
||||
);
|
||||
throw const ApiError.noNetworkConnection();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload 且 path 以 http 开头 → 直接用全路径(S3 presigned URL)
|
||||
// 否则 → 拼接 baseURL
|
||||
final isUpload = request.requestType == ApiRequestType.upload;
|
||||
final path = request.path;
|
||||
final url = (isUpload && path.startsWith('http')) ? path : '${apiClient.config.baseURL}$path';
|
||||
|
||||
// 将请求元数据写入 extra,供拦截器读取
|
||||
final options = Options(
|
||||
method: request.method.value,
|
||||
extra: {
|
||||
'requestType': request.requestType,
|
||||
'includeToken': request.includeToken,
|
||||
'customHeaders': request.customHeaders,
|
||||
},
|
||||
);
|
||||
|
||||
// 访问 parameters 触发代码生成器的 fromJson 注册
|
||||
// (@ApiRequest 生成的 mixin 在 parameters getter 中注册响应类型)
|
||||
final params = request.parameters;
|
||||
|
||||
// GET → queryParameters;POST/PUT/DELETE/PATCH → JSON body;Upload → uploadData
|
||||
final isGet = request.method == HttpMethod.get;
|
||||
final response = await apiClient.dio.request(
|
||||
url,
|
||||
data: isUpload ? request.uploadData : (isGet ? null : params),
|
||||
queryParameters: isGet ? params : null,
|
||||
options: options,
|
||||
);
|
||||
|
||||
// 解码响应(Upload 类型通常需要 override decodeResponse)
|
||||
return request.decodeResponse(response);
|
||||
} on DioException catch (e) {
|
||||
throw apiClient.mapDioError(e);
|
||||
} on ApiError {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw ApiError.unknown(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// WebSocket 长连接客户端
|
||||
///
|
||||
/// 提供:
|
||||
/// - 连接 / 断连 / 自动重连(指数退避)
|
||||
/// - 双层心跳(底层 ping + 应用层 heartbeat)
|
||||
/// - Stream 输出(消息、连接状态、错误)
|
||||
/// - 生命周期感知(前后台切换)
|
||||
///
|
||||
/// ## 使用方式
|
||||
///
|
||||
/// ```dart
|
||||
/// final client = SocketClient(config: socketConfig);
|
||||
///
|
||||
/// // 连接
|
||||
/// await client.connect('wss://api.example.com/ws', token: 'xxx');
|
||||
///
|
||||
/// // 收消息
|
||||
/// client.messageStream.listen((msg) => print(msg));
|
||||
///
|
||||
/// // 发消息
|
||||
/// await client.send({'type': 'chat', 'data': {...}});
|
||||
///
|
||||
/// // 断连
|
||||
/// await client.disconnect();
|
||||
/// ```
|
||||
class SocketClient {
|
||||
final SocketConfig config;
|
||||
|
||||
// ── 内部状态 ──
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _channelSubscription;
|
||||
SocketConnectionState _connectionState = SocketConnectionState.disconnected;
|
||||
String? _currentUrl;
|
||||
String? _currentToken;
|
||||
bool _manualDisconnect = false;
|
||||
bool _isBackground = false;
|
||||
|
||||
// ── 心跳 ──
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _pongTimeoutTimer;
|
||||
bool _waitingForPong = false;
|
||||
|
||||
// ── 重连 ──
|
||||
int _reconnectAttempts = 0;
|
||||
Timer? _reconnectTimer;
|
||||
final _random = Random();
|
||||
|
||||
// ── Stream Controllers ──
|
||||
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _rawMessageController = StreamController<String>.broadcast();
|
||||
final _connectionStateController =
|
||||
StreamController<SocketConnectionState>.broadcast();
|
||||
final _errorController = StreamController<SocketError>.broadcast();
|
||||
|
||||
SocketClient({required this.config});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 公开 API — 连接
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 连接到 WebSocket 服务器
|
||||
///
|
||||
/// [url] — 完整的 WebSocket URL(如 `wss://api.example.com/ws`)
|
||||
/// [token] — 可选,拼接到 URL query 参数
|
||||
Future<bool> connect(String url, {String? token}) async {
|
||||
if (_connectionState == SocketConnectionState.connected ||
|
||||
_connectionState == SocketConnectionState.connecting) {
|
||||
_log('Already connected or connecting, skip');
|
||||
return _connectionState == SocketConnectionState.connected;
|
||||
}
|
||||
|
||||
_currentUrl = url;
|
||||
_currentToken = token;
|
||||
_manualDisconnect = false;
|
||||
|
||||
return _doConnect();
|
||||
}
|
||||
|
||||
/// 断开连接
|
||||
///
|
||||
/// 手动断连不触发自动重连。
|
||||
Future<void> disconnect() async {
|
||||
_manualDisconnect = true;
|
||||
await _doDisconnect(reason: 'Manual disconnect');
|
||||
}
|
||||
|
||||
/// 当前是否已连接
|
||||
bool get isConnected =>
|
||||
_connectionState == SocketConnectionState.connected;
|
||||
|
||||
/// 当前连接状态
|
||||
SocketConnectionState get connectionState => _connectionState;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 公开 API — 发送
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 发送 JSON 消息
|
||||
Future<bool> send(Map<String, dynamic> message) {
|
||||
return sendString(jsonEncode(message));
|
||||
}
|
||||
|
||||
/// 发送原始字符串
|
||||
Future<bool> sendString(String message) async {
|
||||
if (!isConnected || _channel == null) {
|
||||
_emitError(SocketError.sendFailed('Not connected'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
_channel!.sink.add(message);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_emitError(SocketError.sendFailed(e.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 公开 API — Stream 输出
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 已解析的 JSON 消息流
|
||||
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
||||
|
||||
/// 原始字符串消息流(JSON 解析失败的也走这里)
|
||||
Stream<String> get rawMessageStream => _rawMessageController.stream;
|
||||
|
||||
/// 连接状态变化流
|
||||
Stream<SocketConnectionState> get connectionStateStream =>
|
||||
_connectionStateController.stream;
|
||||
|
||||
/// 错误流
|
||||
Stream<SocketError> get errorStream => _errorController.stream;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 公开 API — 生命周期
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// App 回前台 → 检查连接,断了就重连
|
||||
void onEnterForeground() {
|
||||
_isBackground = false;
|
||||
_log('Enter foreground');
|
||||
|
||||
if (!isConnected && !_manualDisconnect && _currentUrl != null) {
|
||||
_log('Connection lost while in background, reconnecting...');
|
||||
_startReconnect();
|
||||
} else if (isConnected) {
|
||||
_startHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
/// App 进后台 → 停止心跳(保持连接)
|
||||
void onEnterBackground() {
|
||||
_isBackground = true;
|
||||
_log('Enter background, pausing heartbeat');
|
||||
_stopHeartbeat();
|
||||
}
|
||||
|
||||
/// 释放所有资源
|
||||
Future<void> dispose() async {
|
||||
_manualDisconnect = true;
|
||||
await _doDisconnect(reason: 'Dispose');
|
||||
await _messageController.close();
|
||||
await _rawMessageController.close();
|
||||
await _connectionStateController.close();
|
||||
await _errorController.close();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部 — 连接
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<bool> _doConnect() async {
|
||||
final url = _currentUrl;
|
||||
if (url == null || url.isEmpty) {
|
||||
_emitError(SocketError.invalidURL(url ?? ''));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证 URL
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || (!uri.isScheme('ws') && !uri.isScheme('wss'))) {
|
||||
_emitError(SocketError.invalidURL(url));
|
||||
return false;
|
||||
}
|
||||
|
||||
_updateConnectionState(SocketConnectionState.connecting);
|
||||
_log('Connecting to $url');
|
||||
|
||||
try {
|
||||
// 构建最终 URL(拼接 token)
|
||||
final connectUri = _currentToken != null
|
||||
? uri.replace(
|
||||
queryParameters: {
|
||||
...uri.queryParameters,
|
||||
'token': _currentToken!,
|
||||
},
|
||||
)
|
||||
: uri;
|
||||
|
||||
// 创建 WebSocket 连接
|
||||
_channel = IOWebSocketChannel.connect(
|
||||
connectUri,
|
||||
pingInterval: config.pingInterval,
|
||||
);
|
||||
|
||||
// 等待连接就绪
|
||||
await _channel!.ready.timeout(config.connectTimeout);
|
||||
|
||||
_log('Connected');
|
||||
_updateConnectionState(SocketConnectionState.connected);
|
||||
_reconnectAttempts = 0;
|
||||
|
||||
// 开始监听消息
|
||||
_channelSubscription = _channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDone,
|
||||
);
|
||||
|
||||
// 启动心跳
|
||||
_startHeartbeat();
|
||||
|
||||
return true;
|
||||
} on TimeoutException {
|
||||
_log('Connection timeout');
|
||||
_emitError(const SocketError.connectionTimeout());
|
||||
await _doDisconnect(reason: 'Timeout');
|
||||
_startReconnect();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_log('Connection failed: $e');
|
||||
_emitError(SocketError.connectionFailed(e.toString()));
|
||||
await _doDisconnect(reason: e.toString());
|
||||
_startReconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doDisconnect({String? reason}) async {
|
||||
_stopHeartbeat();
|
||||
_stopReconnectTimer();
|
||||
|
||||
await _channelSubscription?.cancel();
|
||||
_channelSubscription = null;
|
||||
|
||||
try {
|
||||
await _channel?.sink.close();
|
||||
} catch (_) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
_channel = null;
|
||||
|
||||
if (_connectionState != SocketConnectionState.disconnected) {
|
||||
_log('Disconnected${reason != null ? ': $reason' : ''}');
|
||||
_updateConnectionState(SocketConnectionState.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部 — 消息处理
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _handleMessage(dynamic data) {
|
||||
if (data is! String) {
|
||||
// 非字符串消息(如二进制),走 rawMessageStream
|
||||
_rawMessageController.add(data.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 pong 心跳回复
|
||||
if (data == 'pong') {
|
||||
_onPongReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试 JSON 解析
|
||||
try {
|
||||
final json = jsonDecode(data) as Map<String, dynamic>;
|
||||
_messageController.add(json);
|
||||
} catch (_) {
|
||||
// JSON 解析失败,走原始消息流
|
||||
_rawMessageController.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleError(Object error) {
|
||||
_log('Stream error: $error');
|
||||
_emitError(SocketError.connectionFailed(error.toString()));
|
||||
}
|
||||
|
||||
void _handleDone() async {
|
||||
_log('Stream closed');
|
||||
await _doDisconnect(reason: 'Stream closed');
|
||||
|
||||
if (!_manualDisconnect) {
|
||||
_startReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部 — 心跳
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _startHeartbeat() {
|
||||
_stopHeartbeat();
|
||||
|
||||
_heartbeatTimer = Timer.periodic(config.heartbeatInterval, (_) {
|
||||
if (!isConnected || _isBackground) return;
|
||||
_sendPing();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
_pongTimeoutTimer?.cancel();
|
||||
_pongTimeoutTimer = null;
|
||||
_waitingForPong = false;
|
||||
}
|
||||
|
||||
void _sendPing() {
|
||||
if (_waitingForPong) return;
|
||||
|
||||
_waitingForPong = true;
|
||||
_channel?.sink.add('ping');
|
||||
|
||||
// 启动 pong 超时计时器
|
||||
_pongTimeoutTimer = Timer(config.pongTimeout, () {
|
||||
if (_waitingForPong) {
|
||||
_log('Pong timeout, reconnecting...');
|
||||
_waitingForPong = false;
|
||||
_emitError(const SocketError.pingTimeout());
|
||||
_doDisconnect(reason: 'Pong timeout');
|
||||
_startReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onPongReceived() {
|
||||
_waitingForPong = false;
|
||||
_pongTimeoutTimer?.cancel();
|
||||
_pongTimeoutTimer = null;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部 — 重连(指数退避)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _startReconnect() {
|
||||
if (_manualDisconnect || !config.autoReconnect || _isBackground) return;
|
||||
if (_connectionState == SocketConnectionState.reconnecting) return;
|
||||
|
||||
if (_reconnectAttempts >= config.maxReconnectAttempts) {
|
||||
_log('Max reconnect attempts reached ($_reconnectAttempts)');
|
||||
_reconnectAttempts = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_updateConnectionState(SocketConnectionState.reconnecting);
|
||||
_reconnectAttempts++;
|
||||
|
||||
// 指数退避 + jitter:min(2^attempt * 1s, maxReconnectDelay) + random jitter
|
||||
// jitter 防止多设备同时重连导致服务器瞬间过载(thundering herd)
|
||||
final baseDelayMs = min(
|
||||
pow(2, _reconnectAttempts).toInt() * 1000,
|
||||
config.maxReconnectDelay.inMilliseconds,
|
||||
);
|
||||
final jitterMs = _random.nextInt((baseDelayMs * 0.25).toInt().clamp(1, 7500));
|
||||
final delay = Duration(milliseconds: baseDelayMs + jitterMs);
|
||||
|
||||
_log('Reconnecting in ${delay.inMilliseconds}ms '
|
||||
'(attempt $_reconnectAttempts/${config.maxReconnectAttempts})');
|
||||
|
||||
_reconnectTimer = Timer(delay, () async {
|
||||
// 重连前检查网络
|
||||
if (config.onCheckNetworkAvailable != null) {
|
||||
final available = await config.onCheckNetworkAvailable!();
|
||||
if (!available) {
|
||||
_log('Network unavailable, skip reconnect');
|
||||
_emitError(const SocketError.networkUnavailable());
|
||||
_updateConnectionState(SocketConnectionState.disconnected);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_doConnect();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopReconnectTimer() {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部 — 辅助
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _updateConnectionState(SocketConnectionState newState) {
|
||||
if (_connectionState == newState) return;
|
||||
_connectionState = newState;
|
||||
_connectionStateController.add(newState);
|
||||
}
|
||||
|
||||
void _emitError(SocketError error) {
|
||||
_errorController.add(error);
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
config.onLog?.call(message, tag: 'Socket');
|
||||
}
|
||||
}
|
||||
160
packages/networks_sdk/lib/src/data/dto/api_requestable.dart
Normal file
160
packages/networks_sdk/lib/src/data/dto/api_requestable.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_response_wrapper.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/http_method.dart';
|
||||
|
||||
|
||||
/// API 请求基类
|
||||
///
|
||||
/// 使用侧只需:字段 + path + method,其余全部有默认实现。
|
||||
///
|
||||
/// ```dart
|
||||
/// @JsonSerializable()
|
||||
/// class LoginRequest extends ApiRequestable<LoginData> {
|
||||
/// final String email;
|
||||
/// final String password;
|
||||
///
|
||||
/// LoginRequest({required this.email, required this.password});
|
||||
///
|
||||
/// factory LoginRequest.fromJson(Map<String, dynamic> json) =>
|
||||
/// _$LoginRequestFromJson(json);
|
||||
/// @override
|
||||
/// Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||
///
|
||||
/// @override
|
||||
/// String get path => '/auth/login';
|
||||
/// @override
|
||||
/// HttpMethod get method => HttpMethod.post;
|
||||
/// @override
|
||||
/// bool get includeToken => false;
|
||||
/// }
|
||||
///
|
||||
/// // 文件顶层注册一次(一行)
|
||||
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
|
||||
/// ```
|
||||
abstract class ApiRequestable<T> {
|
||||
/// API 路径(如 '/auth/login')
|
||||
String get path;
|
||||
|
||||
/// HTTP 方法
|
||||
HttpMethod get method;
|
||||
|
||||
/// 序列化为 JSON(由 @JsonSerializable 自动生成)
|
||||
/// 子类 override 返回 `_$XxxToJson(this)` 即可
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
/// 请求参数 — 默认调用 toJson(),upload 类型返回 null
|
||||
/// 绝大多数情况无需 override
|
||||
Map<String, dynamic>? get parameters =>
|
||||
requestType == ApiRequestType.upload ? null : toJson();
|
||||
|
||||
/// 上传请求的 body — upload 类型 override 此方法
|
||||
///
|
||||
/// 支持多种上传格式:
|
||||
/// - `FormData` — multipart 表单上传到自有后端
|
||||
/// - `Uint8List` — 二进制流上传到 S3 presigned URL
|
||||
/// - 其他 Dio 支持的类型
|
||||
///
|
||||
/// ```dart
|
||||
/// // FormData 上传
|
||||
/// @override
|
||||
/// Object? get uploadData => FormData.fromMap({
|
||||
/// 'file': MultipartFile.fromFileSync(filePath),
|
||||
/// });
|
||||
///
|
||||
/// // 二进制上传(S3 presigned URL)
|
||||
/// @override
|
||||
/// Object? get uploadData => fileBytes; // Uint8List
|
||||
/// ```
|
||||
Object? get uploadData => null;
|
||||
|
||||
/// 自定义 headers(合并时覆盖默认值)
|
||||
Map<String, String>? get customHeaders => null;
|
||||
|
||||
/// 请求类型(默认 request,登录 override 为 login)
|
||||
ApiRequestType get requestType => ApiRequestType.request;
|
||||
|
||||
/// 是否在 header 中包含 token(默认 true)
|
||||
bool get includeToken => true;
|
||||
|
||||
/// 解码响应 — 自动从 fromJson 注册表查找解码器
|
||||
///
|
||||
/// **无需 override**,只要注册了响应类型就能自动解码。
|
||||
/// 支持无响应数据的接口(如 logout):data 为 null 时不查注册表,直接返回 null。
|
||||
T? decodeResponse(Response response) {
|
||||
try {
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
// fromJson 查找延迟到实际需要解码时执行
|
||||
// → 无 data 的接口(logout / delete)不会触发 StateError
|
||||
T fromJsonObject(Object? json) {
|
||||
final fromJsonFunc = fromJsonRegistry[T];
|
||||
|
||||
if (fromJsonFunc == null) {
|
||||
throw StateError(
|
||||
'fromJson not registered for type $T. '
|
||||
'Add: final _reg = registerResponse<$T>($T.fromJson);',
|
||||
);
|
||||
}
|
||||
|
||||
if (fromJsonFunc is T Function(Object?)) {
|
||||
return fromJsonFunc(json);
|
||||
}
|
||||
if (json is Map<String, dynamic>) {
|
||||
final mapFunc = fromJsonFunc as T Function(Map<String, dynamic>);
|
||||
return mapFunc(json);
|
||||
}
|
||||
throw FormatException('Expected Map<String, dynamic>, got ${json.runtimeType}',);
|
||||
}
|
||||
|
||||
final wrapper = ApiResponseWrapper<T>.fromJson(data, fromJsonObject);
|
||||
|
||||
// 业务错误码检查
|
||||
if (wrapper.code != 0) {
|
||||
throw ApiError.apiError(
|
||||
code: wrapper.code,
|
||||
message: wrapper.message ?? 'API error (code: ${wrapper.code})',
|
||||
);
|
||||
}
|
||||
|
||||
return wrapper.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (e is ApiError) rethrow;
|
||||
throw ApiError.decodingError(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── fromJson 注册表 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// 全局 fromJson 注册表
|
||||
final fromJsonRegistry = <Type, Function>{};
|
||||
|
||||
/// 注册响应类型(标准 `Map<String, dynamic>` 类型)
|
||||
///
|
||||
/// 在定义 Response 类的文件顶层调用一次即可:
|
||||
/// ```dart
|
||||
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
|
||||
/// ```
|
||||
T Function(Map<String, dynamic>)? registerResponse<T>(T Function(Map<String, dynamic>) fromJson,)
|
||||
{
|
||||
fromJsonRegistry[T] = fromJson;
|
||||
return fromJson;
|
||||
}
|
||||
|
||||
/// 注册响应类型(`Object?` 类型,支持 List 等复杂结构)
|
||||
///
|
||||
/// ```dart
|
||||
/// final _reg = registerResponseObject<DeviceList>(DeviceList.fromJson);
|
||||
/// ```
|
||||
T Function(Object?)? registerResponseObject<T>(
|
||||
T Function(Object?) fromJson,
|
||||
) {
|
||||
fromJsonRegistry[T] = fromJson;
|
||||
return fromJson;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/// API 响应信封解析器
|
||||
/// 统一处理 { code, message/msg, data } 格式的服务器响应
|
||||
class ApiResponseWrapper<T> {
|
||||
final int code;
|
||||
final String? message;
|
||||
final T? data;
|
||||
|
||||
const ApiResponseWrapper({
|
||||
required this.code,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory ApiResponseWrapper.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object?) fromJsonT,
|
||||
) {
|
||||
// code 字段:兼容 int 和 String
|
||||
final int codeValue;
|
||||
if (json['code'] is int) {
|
||||
codeValue = json['code'] as int;
|
||||
} else if (json['code'] is String) {
|
||||
codeValue = int.tryParse(json['code'] as String) ?? 0;
|
||||
} else {
|
||||
throw FormatException(
|
||||
'Expected int or String for code, got ${json['code'].runtimeType}',
|
||||
);
|
||||
}
|
||||
|
||||
// message 字段:兼容 message 和 msg
|
||||
final message =
|
||||
json['message'] as String? ?? json['msg'] as String?;
|
||||
|
||||
// 解码 data(null-safe:logout / delete 等接口可能无 data)
|
||||
final rawData = json['data'];
|
||||
final T? decodedData = rawData != null ? fromJsonT(rawData) : null;
|
||||
|
||||
return ApiResponseWrapper<T>(
|
||||
code: codeValue,
|
||||
message: message,
|
||||
data: decodedData,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
/// Data Transfer Object
|
||||
/// - 只負責資料傳輸 / 解析
|
||||
/// - 結構可變
|
||||
/// - 可以依賴 JSON / platform
|
||||
class NetworksSdkPermissionStatusDto {
|
||||
final bool granted;
|
||||
final bool permanentlyDenied;
|
||||
final String? grantedAt; // 通常是 raw string
|
||||
|
||||
NetworksSdkPermissionStatusDto({
|
||||
required this.granted,
|
||||
required this.permanentlyDenied,
|
||||
this.grantedAt,
|
||||
});
|
||||
|
||||
factory NetworksSdkPermissionStatusDto.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return NetworksSdkPermissionStatusDto(
|
||||
granted: json['granted'] as bool,
|
||||
permanentlyDenied: json['permanentlyDenied'] as bool,
|
||||
grantedAt: json['grantedAt'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:networks_sdk/src/data/datasources/socket/socket_client.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||
import 'package:networks_sdk/src/domain/repositories/networks_messaging_repository.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||
|
||||
/// Messaging Repository Implementation (Data)
|
||||
class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
SocketClient? _socketClient;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initialize(SocketConfig config) {
|
||||
_socketClient = SocketClient(config: config);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
void _checkInitialized() {
|
||||
if (!_isInitialized || _socketClient == null) {
|
||||
throw StateError(
|
||||
'NetworksMessagingRepository not initialized. Call initialize() first.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> connect(String url, {String? token}) {
|
||||
_checkInitialized();
|
||||
return _socketClient!.connect(url, token: token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() {
|
||||
_checkInitialized();
|
||||
return _socketClient!.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isConnected {
|
||||
_checkInitialized();
|
||||
return _socketClient!.isConnected;
|
||||
}
|
||||
|
||||
@override
|
||||
SocketConnectionState get connectionState {
|
||||
_checkInitialized();
|
||||
return _socketClient!.connectionState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> send(Map<String, dynamic> message) {
|
||||
_checkInitialized();
|
||||
return _socketClient!.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> sendString(String message) {
|
||||
_checkInitialized();
|
||||
return _socketClient!.sendString(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Map<String, dynamic>> get messageStream {
|
||||
_checkInitialized();
|
||||
return _socketClient!.messageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> get rawMessageStream {
|
||||
_checkInitialized();
|
||||
return _socketClient!.rawMessageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SocketConnectionState> get connectionStateStream {
|
||||
_checkInitialized();
|
||||
return _socketClient!.connectionStateStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SocketError> get errorStream {
|
||||
_checkInitialized();
|
||||
return _socketClient!.errorStream;
|
||||
}
|
||||
|
||||
@override
|
||||
void onEnterForeground() {
|
||||
_checkInitialized();
|
||||
_socketClient!.onEnterForeground();
|
||||
}
|
||||
|
||||
@override
|
||||
void onEnterBackground() {
|
||||
_checkInitialized();
|
||||
_socketClient!.onEnterBackground();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (_socketClient != null) {
|
||||
await _socketClient!.dispose();
|
||||
_socketClient = null;
|
||||
}
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//Repository Impl
|
||||
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
import '../../domain/repositories/networks_sdk_repository.dart';
|
||||
import '../datasources/networks_sdk_method_channel_datasource.dart';
|
||||
|
||||
class NetworksSdkRepositoryImpl implements NetworksSdkRepository
|
||||
{
|
||||
final NetworksSdkMethodChannelDataSource _datasource;
|
||||
|
||||
const NetworksSdkRepositoryImpl(this._datasource);
|
||||
|
||||
@override
|
||||
Future<String?> platformVersion() {
|
||||
return _datasource.getPlatformVersion();
|
||||
}
|
||||
|
||||
@override
|
||||
void initialize(ApiConfig apiConfig){
|
||||
_datasource.initialize(apiConfig);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) {
|
||||
return _datasource.executeRequest(request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user