Интеграция TradingView Lightweight Charts в мобильное приложение биржи
TradingView Lightweight Charts — JavaScript-библиотека для финансовых графиков весом ~45KB gzip. Её используют в продакшн Coinbase, OKX, Gate.io. На мобильном — запускается внутри WebView, что даёт все возможности библиотеки без нативной реализации candlestick с нуля.
Архитектура: WebView-мост
Интеграция строится на двустороннем мосту: нативное приложение отправляет данные в WebView через JavaScript, WebView сигнализирует обратно о событиях (tap на свечу, crosshair movement).
На Flutter — webview_flutter (официальный от Google):
// Инициализация WebViewController
late final WebViewController _webViewController;
@override
void initState() {
super.initState();
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FlutterBridge',
onMessageReceived: (message) {
final data = jsonDecode(message.message);
if (data['type'] == 'crosshair') {
_onCrosshairUpdate(data['candle']);
}
},
)
..loadFlutterAsset('assets/chart/index.html');
}
// Отправка данных в WebView
Future<void> setChartData(List<Candle> candles) async {
final json = jsonEncode(candles.map((c) => {
'time': c.timestamp ~/ 1000, // Lightweight Charts ожидает секунды
'open': c.open,
'high': c.high,
'low': c.low,
'close': c.close,
}).toList());
await _webViewController.runJavaScript('window.setData($json)');
}
HTML/JS часть: инициализация и API
Минимальный assets/chart/index.html:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #131722; overflow: hidden; }
#chart { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="chart"></div>
<script src="lightweight-charts.standalone.production.js"></script>
<script>
const chart = LightweightCharts.createChart(document.getElementById('chart'), {
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
grid: { vertLines: { color: '#1e2130' }, horzLines: { color: '#1e2130' } },
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
rightPriceScale: { borderColor: '#2a2e39' },
timeScale: { borderColor: '#2a2e39', timeVisible: true, secondsVisible: false },
});
const candleSeries = chart.addCandlestickSeries({
upColor: '#26a69a', downColor: '#ef5350',
borderDownColor: '#ef5350', borderUpColor: '#26a69a',
wickDownColor: '#ef5350', wickUpColor: '#26a69a',
});
const volumeSeries = chart.addHistogramSeries({
color: '#26a69a',
priceFormat: { type: 'volume' },
priceScaleId: 'volume',
scaleMargins: { top: 0.8, bottom: 0 },
});
// Подписка на crosshair — отправляем данные в Flutter
chart.subscribeCrosshairMove(param => {
if (param.seriesData.has(candleSeries)) {
const candle = param.seriesData.get(candleSeries);
FlutterBridge.postMessage(JSON.stringify({ type: 'crosshair', candle }));
}
});
// Вызывается из Flutter
window.setData = function(candles) {
candleSeries.setData(candles);
// Volume отдельно
const volumeData = candles.map(c => ({
time: c.time,
value: c.volume || 0,
color: c.close >= c.open ? '#26a69a44' : '#ef535044',
}));
volumeSeries.setData(volumeData);
chart.timeScale().fitContent();
};
window.updateLastCandle = function(candle) {
candleSeries.update(candle);
};
window.setTimeframe = function(timeframe) {
// Запрос новых данных обрабатывается на Flutter стороне
FlutterBridge.postMessage(JSON.stringify({ type: 'timeframe_change', timeframe }));
};
</script>
</body>
</html>
Real-time обновления
WebSocket тик → Flutter → вызов updateLastCandle в WebView:
void onTickReceived(Tick tick) {
_updateLocalCandle(tick);
final candleJson = jsonEncode({
'time': _lastCandle.timestamp ~/ 1000,
'open': _lastCandle.open,
'high': _lastCandle.high,
'low': _lastCandle.low,
'close': _lastCandle.close,
});
_webViewController.runJavaScript('window.updateLastCandle($candleJson)');
}
candleSeries.update() в Lightweight Charts обновляет только последнюю свечу без перерисовки всего графика. Это оптимизировано — библиотека делает это правильно.
Подводные камни интеграции
Viewport meta. Без maximum-scale=1.0 iOS Safari включает пользовательский zoom на двойной тап — интерфейс расползается. На Android — WebSettings.setSupportZoom(false).
Белый flash при загрузке. WebView рендерит белый фон до загрузки HTML. Решение — backgroundColor у WebView совпадает с фоном графика (#131722), и показываем CircularProgressIndicator поверх WebView до получения onPageFinished.
Задержка первого рендера. WebView инициализируется дольше нативных виджетов — 200-500ms. Для биржи это неприятно. Решение: инициализировать WebView заранее (при открытии экрана тикера, не при переходе на экран графика), прогревать через offscreen WebView.
Keyboard и Focus. WebView перехватывает focus — нативная клавиатура и жесты могут конфликтовать. Явно отключаем text input в WebView: webViewController.setOnPlatformPermissionRequest и не включаем JavaScript form elements.
JavaScript Bridge на iOS. На iOS WKWebView (под капотом WebView) асинхронно доставляет сообщения из JS. При быстром потоке тиков (>10/сек) — очередь сообщений может создавать lag. Решение: батчинг обновлений на Flutter стороне, отправка не каждого тика, а накопленного обновления каждые 100ms.
Технические индикаторы
Lightweight Charts поддерживает добавление произвольных line series поверх основного графика. MA(20) — вычисляем на Flutter, передаём массивом в addLineSeries().setData():
List<Map> calculateMA(List<Candle> candles, int period) {
final result = <Map>[];
for (var i = period - 1; i < candles.length; i++) {
final avg = candles.sublist(i - period + 1, i + 1)
.map((c) => c.close)
.reduce((a, b) => a + b) / period;
result.add({'time': candles[i].timestamp ~/ 1000, 'value': avg});
}
return result;
}
Что входит в работу
- Настройка WebView с правильными параметрами для iOS и Android
- HTML/JS шаблон с Lightweight Charts, настройка темы и серий
- Двусторонний мост Flutter ↔ WebView
- Real-time обновления через WebSocket
- Crosshair с отображением OHLCV в нативной панели Flutter
- Переключение таймфреймов
- Volume bars
- Базовые индикаторы (MA, EMA — по согласованию)
Сроки
Базовая интеграция с WebSocket и crosshair: 5–8 дней. Полноценный экран с переключением таймфреймов, индикаторами, адаптацией под iOS/Android: 2–3 недели. Стоимость рассчитывается индивидуально.







