TradingView Lightweight Charts Integration in Mobile Exchange App
TradingView Lightweight Charts — JavaScript library for financial charts weighing ~45KB gzip. Used in production by Coinbase, OKX, Gate.io. On mobile — runs inside WebView, giving all library capabilities without implementing native candlestick from scratch.
Architecture: WebView Bridge
Integration built on bidirectional bridge: native app sends data to WebView via JavaScript, WebView signals back on events (candle tap, crosshair movement).
On Flutter — webview_flutter (official from Google):
// WebViewController initialization
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');
}
// Send data to WebView
Future<void> setChartData(List<Candle> candles) async {
final json = jsonEncode(candles.map((c) => {
'time': c.timestamp ~/ 1000, // Lightweight Charts expects seconds
'open': c.open,
'high': c.high,
'low': c.low,
'close': c.close,
}).toList());
await _webViewController.runJavaScript('window.setData($json)');
}
HTML/JS Part: Initialization and API
Minimal 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 },
});
// Subscribe to crosshair — send data to Flutter
chart.subscribeCrosshairMove(param => {
if (param.seriesData.has(candleSeries)) {
const candle = param.seriesData.get(candleSeries);
FlutterBridge.postMessage(JSON.stringify({ type: 'crosshair', candle }));
}
});
// Called from Flutter
window.setData = function(candles) {
candleSeries.setData(candles);
// Volume separately
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) {
// Request new data handled on Flutter side
FlutterBridge.postMessage(JSON.stringify({ type: 'timeframe_change', timeframe }));
};
</script>
</body>
</html>
Real-Time Updates
WebSocket tick → Flutter → call updateLastCandle in 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() in Lightweight Charts updates only last candle without redrawing entire chart. This is optimized — library does it right.
Integration Pitfalls
Viewport meta. Without maximum-scale=1.0 iOS Safari enables user zoom on double-tap — interface falls apart. On Android — WebSettings.setSupportZoom(false).
White flash on load. WebView renders white background before HTML loads. Solution — backgroundColor of WebView matches chart background (#131722), show CircularProgressIndicator over WebView until onPageFinished.
First render delay. WebView initializes longer than native widgets — 200-500ms. For exchange this is unpleasant. Solution: initialize WebView early (when opening ticker screen, not when navigating to chart screen), warm up via offscreen WebView.
Keyboard and Focus. WebView captures focus — native keyboard and gestures may conflict. Explicitly disable text input in WebView: webViewController.setOnPlatformPermissionRequest and don't enable JavaScript form elements.
JavaScript Bridge on iOS. On iOS WKWebView (under WebView hood) asynchronously delivers JS messages. With fast tick stream (>10/sec) — message queue may create lag. Solution: batching updates on Flutter side, send not each tick but accumulated update every 100ms.
Technical Indicators
Lightweight Charts supports adding arbitrary line series over main chart. MA(20) — compute on Flutter, pass array to 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;
}
Scope of Work
- WebView setup with correct parameters for iOS and Android
- HTML/JS template with Lightweight Charts, theme and series setup
- Bidirectional Flutter ↔ WebView bridge
- Real-time updates via WebSocket
- Crosshair with OHLCV display in native Flutter panel
- Timeframe switching
- Volume bars
- Basic indicators (MA, EMA — by agreement)
Timeframe
Basic integration with WebSocket and crosshair: 5–8 days. Full screen with timeframe switching, indicators, iOS/Android adaptation: 2–3 weeks. Cost calculated individually.







