184 lines
4.6 KiB
Dart
184 lines
4.6 KiB
Dart
|
|
import 'dart:async';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import '../data/models/kline_candle.dart';
|
||
|
|
import '../data/services/kline_service.dart';
|
||
|
|
import '../data/services/kline_websocket_service.dart';
|
||
|
|
|
||
|
|
/// K线状态管理
|
||
|
|
class KlineProvider extends ChangeNotifier {
|
||
|
|
final KlineService _klineService;
|
||
|
|
final KlineWebSocketService _wsService;
|
||
|
|
|
||
|
|
KlineProvider(this._klineService, this._wsService);
|
||
|
|
|
||
|
|
List<KlineCandle> _candles = [];
|
||
|
|
KlineCandle? _currentCandle;
|
||
|
|
String _interval = '1h';
|
||
|
|
String _coinCode = '';
|
||
|
|
bool _isLoading = false;
|
||
|
|
bool _isLoadingMore = false;
|
||
|
|
bool _isConnected = false;
|
||
|
|
String? _error;
|
||
|
|
|
||
|
|
StreamSubscription<KlineCandle>? _wsSubscription;
|
||
|
|
Timer? _pollingTimer;
|
||
|
|
|
||
|
|
// Getters
|
||
|
|
List<KlineCandle> get candles => _candles;
|
||
|
|
KlineCandle? get currentCandle => _currentCandle;
|
||
|
|
String get interval => _interval;
|
||
|
|
String get coinCode => _coinCode;
|
||
|
|
bool get isLoading => _isLoading;
|
||
|
|
bool get isLoadingMore => _isLoadingMore;
|
||
|
|
bool get isConnected => _isConnected;
|
||
|
|
String? get error => _error;
|
||
|
|
|
||
|
|
/// 加载某个币种的K线数据
|
||
|
|
Future<void> loadCoin(String coinCode, {String? interval}) async {
|
||
|
|
_coinCode = coinCode;
|
||
|
|
if (interval != null) _interval = interval;
|
||
|
|
_candles = [];
|
||
|
|
_currentCandle = null;
|
||
|
|
_error = null;
|
||
|
|
_isLoading = true;
|
||
|
|
notifyListeners();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1. 获取历史K线
|
||
|
|
final response = await _klineService.fetchHistory(
|
||
|
|
coinCode: _coinCode,
|
||
|
|
interval: _interval,
|
||
|
|
limit: 200,
|
||
|
|
);
|
||
|
|
if (response.success && response.data != null) {
|
||
|
|
_candles = response.data!;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 获取当前K线
|
||
|
|
final currentResponse = await _klineService.fetchCurrentCandle(
|
||
|
|
coinCode: _coinCode,
|
||
|
|
interval: _interval,
|
||
|
|
);
|
||
|
|
if (currentResponse.success && currentResponse.data != null) {
|
||
|
|
_currentCandle = currentResponse.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 连接 WebSocket
|
||
|
|
_connectWebSocket();
|
||
|
|
|
||
|
|
_isLoading = false;
|
||
|
|
notifyListeners();
|
||
|
|
} catch (e) {
|
||
|
|
_error = '加载K线数据失败: $e';
|
||
|
|
_isLoading = false;
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 切换周期
|
||
|
|
Future<void> changeInterval(String newInterval) async {
|
||
|
|
if (newInterval == _interval) return;
|
||
|
|
|
||
|
|
_interval = newInterval;
|
||
|
|
_candles = [];
|
||
|
|
_currentCandle = null;
|
||
|
|
|
||
|
|
// 重新加载
|
||
|
|
await loadCoin(_coinCode, interval: newInterval);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 加载更多历史数据(分页)
|
||
|
|
Future<void> loadMore() async {
|
||
|
|
if (_isLoadingMore || _candles.isEmpty) return;
|
||
|
|
_isLoadingMore = true;
|
||
|
|
notifyListeners();
|
||
|
|
|
||
|
|
try {
|
||
|
|
final oldestTime = _candles.first.closeTime;
|
||
|
|
final response = await _klineService.fetchHistory(
|
||
|
|
coinCode: _coinCode,
|
||
|
|
interval: _interval,
|
||
|
|
limit: 200,
|
||
|
|
before: oldestTime,
|
||
|
|
);
|
||
|
|
if (response.success && response.data != null) {
|
||
|
|
_candles = [...response.data!, ..._candles];
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
|
||
|
|
_isLoadingMore = false;
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _connectWebSocket() {
|
||
|
|
// 取消之前的订阅
|
||
|
|
_wsSubscription?.cancel();
|
||
|
|
_wsService.unsubscribe(_coinCode);
|
||
|
|
|
||
|
|
// 订阅新币种
|
||
|
|
_wsService.connect();
|
||
|
|
_wsSubscription = _wsService.subscribe(_coinCode).listen(
|
||
|
|
_onTick,
|
||
|
|
onError: (_) => _startPolling(),
|
||
|
|
onDone: () => _startPolling(),
|
||
|
|
);
|
||
|
|
|
||
|
|
_isConnected = _wsService.isConnected;
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _onTick(KlineCandle tick) {
|
||
|
|
if (tick.interval != _interval) return;
|
||
|
|
|
||
|
|
_isConnected = true;
|
||
|
|
|
||
|
|
if (tick.isClosed) {
|
||
|
|
// 收盘 tick → 添加到历史列表
|
||
|
|
_candles.add(tick);
|
||
|
|
_currentCandle = null;
|
||
|
|
|
||
|
|
// 停止轮询(如果之前在轮询)
|
||
|
|
_pollingTimer?.cancel();
|
||
|
|
_pollingTimer = null;
|
||
|
|
} else {
|
||
|
|
// 进行中的 tick → 更新当前K线
|
||
|
|
_currentCandle = tick;
|
||
|
|
}
|
||
|
|
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// WebSocket 断连时降级为 HTTP 轮询
|
||
|
|
void _startPolling() {
|
||
|
|
_isConnected = false;
|
||
|
|
notifyListeners();
|
||
|
|
|
||
|
|
_pollingTimer?.cancel();
|
||
|
|
_pollingTimer = Timer.periodic(
|
||
|
|
const Duration(seconds: 5),
|
||
|
|
(_) => _pollCurrentCandle(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _pollCurrentCandle() async {
|
||
|
|
try {
|
||
|
|
final response = await _klineService.fetchCurrentCandle(
|
||
|
|
coinCode: _coinCode,
|
||
|
|
interval: _interval,
|
||
|
|
);
|
||
|
|
if (response.success && response.data != null) {
|
||
|
|
_currentCandle = response.data;
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_wsSubscription?.cancel();
|
||
|
|
_wsService.unsubscribe(_coinCode);
|
||
|
|
_pollingTimer?.cancel();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
}
|