Initial project

This commit is contained in:
Cody
2026-03-06 14:56:17 +08:00
parent 977b627b15
commit bf9e099747
1180 changed files with 50973 additions and 0 deletions

View File

@@ -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 ----------------
}

View File

@@ -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');
}
}
}

View File

@@ -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);
}
}

View File

@@ -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');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 URLpath 以 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 → queryParametersPOST/PUT/DELETE/PATCH → JSON bodyUpload → 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());
}
}
}

View File

@@ -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++;
// 指数退避 + jittermin(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');
}
}

View 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**,只要注册了响应类型就能自动解码。
/// 支持无响应数据的接口(如 logoutdata 为 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;
}

View File

@@ -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?;
// 解码 datanull-safelogout / delete 等接口可能无 data
final rawData = json['data'];
final T? decodedData = rawData != null ? fromJsonT(rawData) : null;
return ApiResponseWrapper<T>(
code: codeValue,
message: message,
data: decodedData,
);
}
}

View File

@@ -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?,
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}