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,29 @@
library;
export 'src/presentation/facade/networks_sdk_api.dart';
export 'src/presentation/facade/networks_messaging_api.dart';
// Wiring - Implementations
export 'src/presentation/wiring/networks_messaging_api_impl.dart';
// Dio 类型重导出App 层上传 / override decodeResponse 需要,避免直接依赖 dio
export 'package:dio/dio.dart' show FormData, MultipartFile, Response;
// Config
export 'src/presentation/wiring/api_config.dart';
export 'src/presentation/wiring/socket_config.dart';
export 'src/presentation/wiring/network_callbacks.dart';
// Model
export 'src/data/dto/api_requestable.dart';
export 'src/data/dto/api_response_wrapper.dart';
export 'src/domain/entities/api_error.dart';
export 'src/domain/entities/http_method.dart';
export 'src/domain/entities/api_request_type.dart';
// Socket Entities
export 'src/domain/entities/socket_connection_state.dart';
export 'src/domain/entities/socket_error.dart';
// Annotations代码生成
export 'src/annotations/api_request.dart';

View File

@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'networks_sdk_platform_interface.dart';
/// An implementation of [NetworksSdkPlatform] that uses method channels.
class MethodChannelNetworksSdk extends NetworksSdkPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('networks_sdk');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'networks_sdk_method_channel.dart';
abstract class NetworksSdkPlatform extends PlatformInterface {
/// Constructs a NetworksSdkPlatform.
NetworksSdkPlatform() : super(token: _token);
static final Object _token = Object();
static NetworksSdkPlatform _instance = MethodChannelNetworksSdk();
/// The default instance of [NetworksSdkPlatform] to use.
///
/// Defaults to [MethodChannelNetworksSdk].
static NetworksSdkPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [NetworksSdkPlatform] when
/// they register themselves.
static set instance(NetworksSdkPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

View File

@@ -0,0 +1,65 @@
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
import 'package:networks_sdk/src/domain/entities/http_method.dart';
/// API 请求注解 — 标记一个类为 API 请求
///
/// 配合 `build_runner` 代码生成器,自动生成 `ApiRequestable<T>` 协议实现,
/// 使用侧只需定义字段 + 注解path / method / requestType / includeToken
/// 全部由生成器自动提供。
///
/// ## 使用方式
///
/// ```dart
/// @ApiRequest(
/// path: '/auth/login',
/// method: HttpMethod.post,
/// responseType: LoginData,
/// requestType: ApiRequestType.login,
/// )
/// @JsonSerializable()
/// class LoginRequest extends ApiRequestable<LoginData>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
///
/// LoginRequest({required this.email, required this.password});
///
/// @override
/// Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
/// }
/// ```
///
/// 生成器自动生成 `_$LoginRequestApi` mixin提供
/// - `path` → `'/auth/login'`
/// - `method` → `HttpMethod.post`
/// - `requestType` → `ApiRequestType.login`
/// - `includeToken` → `false`login 类型自动设为 false
class ApiRequest {
/// API 路径(如 `'/auth/login'`
final String path;
/// HTTP 方法(默认 POST
final HttpMethod method;
/// 响应类型(用于泛型绑定)
final Type responseType;
/// 请求类型(决定 header 处理方式,默认 request
final ApiRequestType requestType;
/// 是否在 header 中包含 token默认根据 requestType 推断login → false其余 → true
final bool? includeToken;
/// 自定义请求头
final Map<String, String>? customHeaders;
const ApiRequest({
required this.path,
this.method = HttpMethod.post,
required this.responseType,
this.requestType = ApiRequestType.request,
this.includeToken,
this.customHeaders,
});
}

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

View File

@@ -0,0 +1,31 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_error.freezed.dart';
/// API 统一错误类型Freezed 联合类型)
@freezed
class ApiError with _$ApiError implements Exception {
const factory ApiError.noNetworkConnection() = _NoNetworkConnection;
const factory ApiError.timeout() = _Timeout;
const factory ApiError.networkError(String message) = _NetworkError;
const factory ApiError.decodingError(String message) = _DecodingError;
const factory ApiError.apiError({
required int code,
required String message,
}) = _ApiError;
const factory ApiError.unknown(String? message) = _Unknown;
}
/// 错误类型扩展 - 提供用户可读的错误描述
extension ApiErrorExtension on ApiError {
String get displayMessage {
return when(
noNetworkConnection: () => 'No network connection',
timeout: () => 'Request timeout',
networkError: (message) => 'Network error: $message',
decodingError: (message) => 'Decoding error: $message',
apiError: (code, message) => message,
unknown: (message) => message ?? 'Unknown error',
);
}
}

View File

@@ -0,0 +1,11 @@
/// API 请求类型
enum ApiRequestType {
/// 普通请求(包含 token header
request,
/// 登录请求(不包含 token header
login,
/// 文件上传multipart不序列化 parameters
upload,
}

View File

@@ -0,0 +1,11 @@
/// HTTP 方法枚举
enum HttpMethod {
get('GET'),
post('POST'),
put('PUT'),
delete('DELETE'),
patch('PATCH');
final String value;
const HttpMethod(this.value);
}

View File

@@ -0,0 +1,18 @@
/// Domain Entity
/// - 表達「通知權限狀態」這個業務概念
/// - 穩定、可被 Facade 回傳
/// - 不要包含 JSON / platform / plugin
class NetworksSdkPermissionStatus {
final bool isGranted;
final bool isPermanentlyDenied;
final DateTime? grantedAt;
const NetworksSdkPermissionStatus({
required this.isGranted,
required this.isPermanentlyDenied,
this.grantedAt,
});
/// 純業務邏輯允許
bool get canRequestAgain => !isGranted && !isPermanentlyDenied;
}

View File

@@ -0,0 +1,14 @@
/// WebSocket 连接状态
enum SocketConnectionState {
/// 未连接
disconnected,
/// 连接中
connecting,
/// 已连接
connected,
/// 重连中(断线后自动重连)
reconnecting,
}

View File

@@ -0,0 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'socket_error.freezed.dart';
/// WebSocket 统一错误类型Freezed 联合类型)
@freezed
class SocketError with _$SocketError implements Exception {
const factory SocketError.connectionFailed(String message) =
_ConnectionFailed;
const factory SocketError.connectionTimeout() = _ConnectionTimeout;
const factory SocketError.disconnected({String? reason}) = _Disconnected;
const factory SocketError.pingTimeout() = _PingTimeout;
const factory SocketError.networkUnavailable() = _NetworkUnavailable;
const factory SocketError.invalidURL(String url) = _InvalidURL;
const factory SocketError.sendFailed(String message) = _SendFailed;
const factory SocketError.unknown(String? message) = _Unknown;
}
/// 错误类型扩展 — 提供用户可读的错误描述
extension SocketErrorExtension on SocketError {
String get displayMessage {
return when(
connectionFailed: (message) => 'Connection failed: $message',
connectionTimeout: () => 'Connection timeout',
disconnected: (reason) => reason ?? 'Disconnected',
pingTimeout: () => 'Ping timeout',
networkUnavailable: () => 'Network unavailable',
invalidURL: (url) => 'Invalid URL: $url',
sendFailed: (message) => 'Send failed: $message',
unknown: (message) => message ?? 'Unknown error',
);
}
}

View File

@@ -0,0 +1,49 @@
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/presentation/wiring/socket_config.dart';
/// Messaging Repository Interface (Domain)
abstract class NetworksMessagingRepository {
/// Initialize with config
void initialize(SocketConfig config);
/// Connect to messaging server
Future<bool> connect(String url, {String? token});
/// Disconnect from server
Future<void> disconnect();
/// Check if connected
bool get isConnected;
/// Current connection state
SocketConnectionState get connectionState;
/// Send a JSON message
Future<bool> send(Map<String, dynamic> message);
/// Send a raw string message
Future<bool> sendString(String message);
/// Stream of incoming parsed JSON messages
Stream<Map<String, dynamic>> get messageStream;
/// Stream of raw string messages
Stream<String> get rawMessageStream;
/// Stream of connection state changes
Stream<SocketConnectionState> get connectionStateStream;
/// Stream of errors
Stream<SocketError> get errorStream;
/// Called when app enters foreground
void onEnterForeground();
/// Called when app enters background
void onEnterBackground();
/// Dispose all resources
Future<void> dispose();
}

View File

@@ -0,0 +1,12 @@
// Repository InterfaceDomain
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
abstract class NetworksSdkRepository {
Future<String?> platformVersion();
void initialize(ApiConfig apiConfig);
Future<T?> executeRequest<T>(ApiRequestable<T> request);
}

View File

@@ -0,0 +1,15 @@
//UseCase
import '../repositories/networks_sdk_repository.dart';
class PlatformVersion
{
final NetworksSdkRepository _repository;
const PlatformVersion(this._repository);
Future<String?> call() {
return _repository.platformVersion();
}
}

View File

@@ -0,0 +1,108 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:networks_sdk/src/annotations/api_request.dart';
import 'package:source_gen/source_gen.dart';
import 'package:build/build.dart';
/// @ApiRequest 代码生成器
///
/// 为标注了 `@ApiRequest` 的类自动生成 mixin提供
/// - `path`, `method`, `requestType`, `includeToken` 协议实现
/// - 自动注册响应类型的 `fromJson`(在 `parameters` getter 中触发,
/// 保证首次请求前完成注册,无需手动调用 `registerApiResponses()`
///
/// 生成的 mixin 命名规则:`_$<ClassName>Api`
///
/// 示例输出:
/// ```dart
/// mixin _$LoginRequestApi on ApiRequestable<LoginData> {
/// @override String get path => '/auth/login';
/// @override HttpMethod get method => HttpMethod.post;
/// @override ApiRequestType get requestType => ApiRequestType.login;
/// @override bool get includeToken => false;
/// @override
/// Map<String, dynamic>? get parameters {
/// registerResponse<LoginData>(LoginData.fromJson);
/// return super.parameters;
/// }
/// }
/// ```
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
{
@override
String generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep,)
{
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@ApiRequest can only be applied to classes.',
element: element,
);
}
final className = element.name;
final path = annotation.read('path').stringValue;
// 读取 HttpMethod 枚举值
final methodName = _readEnumName(
annotation.read('method').objectValue,
'post',
);
// 读取 responseType用于泛型绑定 + 自动注册 fromJson
final responseType = annotation.read('responseType').typeValue;
final responseTypeName = responseType.getDisplayString();
// 读取 ApiRequestType 枚举值
final requestTypeName = _readEnumName(
annotation.read('requestType').objectValue,
'request',
);
// 读取 includeToken默认根据 requestType 推断login → false其余 → true
final includeTokenReader = annotation.peek('includeToken');
final bool includeToken;
if (includeTokenReader != null && !includeTokenReader.isNull) {
includeToken = includeTokenReader.boolValue;
} else {
includeToken = requestTypeName != 'login';
}
return '''
/// Generated by @ApiRequest for [$className]
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@override
String get path => '$path';
@override
HttpMethod get method => HttpMethod.$methodName;
@override
ApiRequestType get requestType => ApiRequestType.$requestTypeName;
@override
bool get includeToken => $includeToken;
@override
Map<String, dynamic>? get parameters {
registerResponse<$responseTypeName>($responseTypeName.fromJson);
return super.parameters;
}
}
''';
}
/// 从 DartObject 提取枚举常量名称
String _readEnumName(dynamic dartObject, String defaultValue) {
final index = dartObject.getField('index')?.toIntValue();
if (index == null) return defaultValue;
final type = dartObject.type;
if (type is InterfaceType) {
final constants = type.element.fields
.where((f) => f.isEnumConstant)
.toList();
if (index < constants.length) {
return constants[index].name ?? defaultValue;
}
}
return defaultValue;
}
}

View File

@@ -0,0 +1,12 @@
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'api_request_generator.dart';
/// @ApiRequest 代码生成器入口
///
/// 在 `build.yaml` 中注册此 builder配合 `build_runner` 使用。
/// 生成的代码通过 `SharedPartBuilder` 合并到 `.g.dart` 文件中,
/// 与 `json_serializable` 等生成器共存。
Builder apiRequestBuilder(BuilderOptions options) =>
SharedPartBuilder([ApiRequestGenerator()], 'api_request');

View File

@@ -0,0 +1,92 @@
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/presentation/wiring/networks_sdk_wiring.dart';
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
/// Messaging API for real-time communication
///
/// This abstract class provides a technology-agnostic interface for
/// real-time messaging. The actual implementation may use WebSocket
/// or other transport mechanisms.
///
/// ## Usage
///
/// ```dart
/// final messaging = NetworksMessagingApi();
/// await messaging.initialize(SocketConfig(...));
///
/// // Connect to messaging server
/// await messaging.connect('wss://api.example.com/ws', token: 'xxx');
///
/// // Listen for messages
/// messaging.messageStream.listen((msg) => print(msg));
///
/// // Send messages
/// await messaging.send({'type': 'chat', 'data': {...}});
///
/// // Handle connection state
/// messaging.connectionStateStream.listen((state) => ...);
///
/// // Handle errors
/// messaging.errorStream.listen((error) => ...);
///
/// // Lifecycle management
/// messaging.onEnterForeground();
/// messaging.onEnterBackground();
///
/// // Cleanup
/// await messaging.disconnect();
/// await messaging.dispose();
/// ```
abstract class NetworksMessagingApi
{
factory NetworksMessagingApi() => NetworksSdkWiring.buildMessagingApi();
/// Initialize the messaging service with configuration
void initialize(SocketConfig config);
/// Connect to the messaging server
///
/// [url] - WebSocket URL (e.g., 'wss://api.example.com/ws')
/// [token] - Optional authentication token
Future<bool> connect(String url, {String? token});
/// Disconnect from the messaging server
///
/// Manual disconnect does not trigger auto-reconnect
Future<void> disconnect();
/// Check if currently connected
bool get isConnected;
/// Current connection state
SocketConnectionState get connectionState;
/// Send a JSON message
Future<bool> send(Map<String, dynamic> message);
/// Send a raw string message
Future<bool> sendString(String message);
/// Stream of incoming parsed JSON messages
Stream<Map<String, dynamic>> get messageStream;
/// Stream of raw string messages (including failed JSON parses)
Stream<String> get rawMessageStream;
/// Stream of connection state changes
Stream<SocketConnectionState> get connectionStateStream;
/// Stream of errors
Stream<SocketError> get errorStream;
/// Called when app enters foreground
void onEnterForeground();
/// Called when app enters background
void onEnterBackground();
/// Dispose all resources
Future<void> dispose();
}

View File

@@ -0,0 +1,19 @@
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
/// SDK API
abstract class NetworksSdkApi
{
factory NetworksSdkApi() => NetworksSdkWiring.build();
Future<String?> platformVersion();
void initialize(ApiConfig aApiConfig);
Future<T?> executeRequest<T>(ApiRequestable<T> request);
}

View File

@@ -0,0 +1,108 @@
import 'network_callbacks.dart';
/// API 配置
/// 非单例,由 App 层构造并注入到 ApiClient
class ApiConfig {
/// 基础 URL来自 config.json → AppConfig.apiBaseUrl
String baseURL;
/// 当前 token内存持有App 层负责持久化)
String? token;
/// 平台相关 headersApp 层注入version、platform、channel 等)
Map<String, String> platformHeaders;
/// Token 过期时的刷新回调
final OnTokenRefresh? onTokenRefresh;
/// 需要强制登出时的回调
final OnForceLogout? onForceLogout;
/// 日志输出回调(不设置则不输出日志)
final OnLog? onLog;
/// 网络可用性查询App 层注入,请求前调用)
///
/// 与 [SocketConfig.onCheckNetworkAvailable] 对称。
/// 返回 true 表示网络可用,可以发起请求;
/// 返回 false 则直接抛 [ApiError.noNetworkConnection],不走网络。
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
/// App 层定义的 Token 过期错误码集合
final Set<int> tokenExpiredCodes;
/// App 层定义的强制登出错误码集合
final Set<int> forceLogoutCodes;
/// 瞬态错误最大重试次数5xx / 超时 / 连接失败)
///
/// 0 = 不重试(默认),设为 3 启用重试。
/// 与 Token 刷新重试独立,两者可叠加。
final int maxRetries;
/// 重试基础延迟(指数退避起点)
///
/// 实际延迟 = min(baseDelay * 2^attempt, 30s) + jitter
final Duration retryBaseDelay;
ApiConfig({
required this.baseURL,
this.token,
this.platformHeaders = const {},
this.onTokenRefresh,
this.onForceLogout,
this.onLog,
this.onCheckNetworkAvailable,
this.tokenExpiredCodes = const {},
this.forceLogoutCodes = const {},
this.maxRetries = 0,
this.retryBaseDelay = const Duration(seconds: 1),
});
/// 构建默认 headers
/// [includeToken] — 是否注入 token
/// [customHeaders] — 单次请求自定义 header优先级最高
Map<String, String> defaultHeaders({
bool includeToken = true,
Map<String, String>? customHeaders,
}) {
final headers = <String, String>{
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
'Timestamp': '${DateTime.now().millisecondsSinceEpoch ~/ 1000}',
'APP-Request-ID': _generateRequestId(),
};
// 合并平台 headersApp 层注入的 version、platform 等)
headers.addAll(platformHeaders);
// Token 注入
if (includeToken && token != null && token!.isNotEmpty) {
headers['token'] = token!;
}
// 单次请求自定义 header覆盖默认值
if (customHeaders != null) {
headers.addAll(customHeaders);
}
return headers;
}
/// 更新 token
void updateToken(String? newToken) {
token = newToken;
}
/// 更新 base URL
void updateBaseURL(String newBaseURL) {
baseURL = newBaseURL;
}
/// 生成请求 ID用于幂等性
String _generateRequestId() {
final now = DateTime.now().microsecondsSinceEpoch;
return '$now${Object().hashCode}';
}
}

View File

@@ -0,0 +1,13 @@
/// 网络层回调类型定义,由 App 层注入 SDK避免 SDK 直接依赖外部实现。
library;
typedef OnTokenRefresh = Future<String?> Function();
/// 強制登出回調
typedef OnForceLogout = void Function();
/// 日誌輸出回調
typedef OnLog = void Function(String message, {String? tag});
/// 網路可用性查詢App 層注入SDK 在請求前調用)
typedef OnCheckNetworkAvailable = Future<bool> Function();

View File

@@ -0,0 +1,106 @@
import 'package:networks_sdk/src/data/repositories/networks_messaging_repository_impl.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/facade/networks_messaging_api.dart';
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
/// Implementation of [NetworksMessagingApi] using [NetworksMessagingRepository]
class NetworksMessagingApiImpl implements NetworksMessagingApi {
NetworksMessagingRepository? _repository;
@override
void initialize(SocketConfig config) {
_repository = NetworksMessagingRepositoryImpl();
_repository!.initialize(config);
}
void _checkInitialized() {
if (_repository == null) {
throw StateError(
'NetworksMessagingApi not initialized. Call initialize() first.',
);
}
}
@override
Future<bool> connect(String url, {String? token}) {
_checkInitialized();
return _repository!.connect(url, token: token);
}
@override
Future<void> disconnect() {
_checkInitialized();
return _repository!.disconnect();
}
@override
bool get isConnected {
_checkInitialized();
return _repository!.isConnected;
}
@override
SocketConnectionState get connectionState {
_checkInitialized();
return _repository!.connectionState;
}
@override
Future<bool> send(Map<String, dynamic> message) {
_checkInitialized();
return _repository!.send(message);
}
@override
Future<bool> sendString(String message) {
_checkInitialized();
return _repository!.sendString(message);
}
@override
Stream<Map<String, dynamic>> get messageStream {
_checkInitialized();
return _repository!.messageStream;
}
@override
Stream<String> get rawMessageStream {
_checkInitialized();
return _repository!.rawMessageStream;
}
@override
Stream<SocketConnectionState> get connectionStateStream {
_checkInitialized();
return _repository!.connectionStateStream;
}
@override
Stream<SocketError> get errorStream {
_checkInitialized();
return _repository!.errorStream;
}
@override
void onEnterForeground() {
_checkInitialized();
_repository!.onEnterForeground();
}
@override
void onEnterBackground() {
_checkInitialized();
_repository!.onEnterBackground();
}
@override
Future<void> dispose() async {
if (_repository != null) {
await _repository!.dispose();
_repository = null;
}
}
}

View File

@@ -0,0 +1,19 @@
import '../../../networks_sdk.dart';
import 'networks_sdk_core.dart';
/// SDK API Implementation
class NetworksSdkApiImpl implements NetworksSdkApi {
final NetworksSdkCore _core;
NetworksSdkApiImpl({required NetworksSdkCore core}) : _core = core;
@override
Future<String?> platformVersion() => _core.repo.platformVersion();
@override
void initialize(ApiConfig apiConfig) => _core.repo.initialize(apiConfig);
@override
Future<T?> executeRequest<T>(ApiRequestable<T> request) => _core.repo.executeRequest(request);
}

View File

@@ -0,0 +1,14 @@
import '../../../networks_sdk_platform_interface.dart';
import '../../domain/repositories/networks_sdk_repository.dart';
class NetworksSdkCore
{
final NetworksSdkPlatform platform;
final NetworksSdkRepository repo;
NetworksSdkCore({
required this.platform,
required this.repo,
});
}

View File

@@ -0,0 +1,37 @@
import '../../../networks_sdk.dart';
import '../../../networks_sdk_method_channel.dart';
import '../../../networks_sdk_platform_interface.dart';
import '../../data/datasources/networks_sdk_method_channel_datasource.dart';
import '../../data/repositories/networks_sdk_repository_impl.dart';
import 'networks_sdk_core.dart';
import 'networks_sdk_api_impl.dart';
/// SDK Wiring - builds all SDK components
class NetworksSdkWiring
{
/// Builds the HTTP API
static NetworksSdkApi buildApi() {
// platform instancemethod channel
final platform = NetworksSdkPlatform.instance;
if (platform is MethodChannelNetworksSdk) {
// platform.init(); // or defer to NotificationApiImpl.init
}
// data layer
final ds = NetworksSdkMethodChannelDataSource(platform);
final repo = NetworksSdkRepositoryImpl(ds);
final core = NetworksSdkCore(platform: platform, repo: repo,);
return NetworksSdkApiImpl(core: core);
}
/// Builds the messaging API (WebSocket)
static NetworksMessagingApi buildMessagingApi() {
return NetworksMessagingApiImpl();
}
/// Builds the default SDK instance (HTTP API)
/// Use [buildMessagingApi()] separately for messaging features
static NetworksSdkApi build() => buildApi();
}

View File

@@ -0,0 +1,46 @@
/// WebSocket 配置
/// 非单例,由 App 层构造并注入到 SocketClient
///
/// 与 [ApiConfig] 设计一致SDK 不依赖 Flutter
/// 网络检测、生命周期等业务逻辑通过回调注入。
class SocketConfig {
/// 应用层心跳间隔(定时发送 "ping" 字符串)
final Duration heartbeatInterval;
/// 底层 WebSocket ping 间隔Dart WebSocket 自动管理)
final Duration pingInterval;
/// Pong 超时(超过此时间未收到 pong 则判定连接断开)
final Duration pongTimeout;
/// 连接超时
final Duration connectTimeout;
/// 最大重连次数0 = 不重连)
final int maxReconnectAttempts;
/// 最大重连延迟(指数退避上限)
final Duration maxReconnectDelay;
/// 是否自动重连
final bool autoReconnect;
/// 日志输出回调(与 ApiConfig.onLog 同签名)
final void Function(String message, {String? tag})? onLog;
/// 网络可用性查询App 层注入SDK 在重连前调用)
/// 返回 true 表示网络可用,可以尝试重连
final Future<bool> Function()? onCheckNetworkAvailable;
SocketConfig({
this.heartbeatInterval = const Duration(seconds: 10),
this.pingInterval = const Duration(seconds: 5),
this.pongTimeout = const Duration(seconds: 10),
this.connectTimeout = const Duration(seconds: 15),
this.maxReconnectAttempts = 5,
this.maxReconnectDelay = const Duration(seconds: 30),
this.autoReconnect = true,
this.onLog,
this.onCheckNetworkAvailable,
});
}