candlesticks 3.0.1 copy "candlesticks: ^3.0.1" to clipboard
candlesticks: ^3.0.1 copied to clipboard

A high-performance, interactive Flutter candlestick chart for financial apps.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';

import 'package:candlesticks/candlesticks.dart';
import 'package:example/symbol_search_modal.dart';
import 'package:example/toolbar.dart';
import 'package:example/toolbar_action.dart';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import './candle_ticker_model.dart';
import './repository.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static const String _defaultSymbol = 'BTCUSDT';
  static const String _defaultInterval = '4h';

  final BinanceRepository _repository = BinanceRepository();
  final CandlesticksController _controller = CandlesticksController();

  final List<String> _intervals = const [
    '1m',
    '3m',
    '5m',
    '15m',
    '30m',
    '1h',
    '2h',
    '4h',
    '6h',
    '8h',
    '12h',
    '1d',
    '3d',
    '1w',
    '1M',
  ];

  WebSocketChannel? _channel;

  List<Candle> _candles = [];
  List<String> _symbols = [];

  bool _themeIsDark = false;
  String _currentSymbol = '';
  String _currentInterval = _defaultInterval;

  @override
  void initState() {
    super.initState();
    _loadSymbols();
  }

  @override
  void dispose() {
    _closeChannel();
    super.dispose();
  }

  Future<void> _loadSymbols() async {
    try {
      final symbols = await _repository.fetchSymbols();

      if (!mounted) return;

      setState(() {
        _symbols = symbols;
      });

      if (symbols.isNotEmpty) {
        await _loadCandles(_defaultSymbol, _currentInterval);
      }
    } catch (_) {
      // You can show an error message here if needed.
    }
  }

  Future<void> _loadCandles(String symbol, String interval) async {
    _closeChannel();

    setState(() {
      _candles = [];
      _currentInterval = interval;
    });

    try {
      final candles = await _repository.fetchCandles(
        symbol: symbol,
        interval: interval,
      );

      final channel = _repository.establishConnection(
        symbol.toLowerCase(),
        interval,
      );

      if (!mounted) {
        unawaited(channel.sink.close());
        return;
      }

      setState(() {
        _candles = candles;
        _channel = channel;
        _currentSymbol = symbol;
        _currentInterval = interval;
      });
    } catch (_) {
      // You can show an error message here if needed.
    }
  }

  Future<void> _loadMoreCandles() async {
    if (_candles.isEmpty || _currentSymbol.isEmpty) return;

    try {
      final candles = await _repository.fetchCandles(
        symbol: _currentSymbol,
        interval: _currentInterval,
        endTime: _candles.last.date.millisecondsSinceEpoch,
      );

      if (!mounted) return;

      setState(() {
        if (_candles.isNotEmpty) {
          _candles.removeLast();
        }

        _candles.addAll(candles);
      });
    } catch (_) {
      // You can show an error message here if needed.
    }
  }

  void _closeChannel() {
    _channel?.sink.close();
    _channel = null;
  }

  void _toggleTheme() {
    setState(() {
      _themeIsDark = !_themeIsDark;
    });
  }

  void _handleSocketSnapshot(AsyncSnapshot<Object?> snapshot) {
    final data = snapshot.data;

    if (_candles.isEmpty || data == null || data is! String) return;

    final candle = _parseCandleFromSocketData(data);
    if (candle == null) return;

    setState(() {
      _upsertLiveCandle(candle);
    });
  }

  Candle? _parseCandleFromSocketData(String data) {
    try {
      final json = jsonDecode(data) as Map<String, dynamic>;

      if (!json.containsKey('k')) return null;

      return CandleTickerModel.fromJson(json).candle;
    } catch (_) {
      return null;
    }
  }

  void _upsertLiveCandle(Candle candle) {
    if (_candles.isEmpty) return;

    final latestCandle = _candles.first;

    final isLatestCandleUpdate =
        latestCandle.date == candle.date && latestCandle.open == candle.open;

    if (isLatestCandleUpdate) {
      _candles[0] = candle;
      return;
    }

    if (_candles.length < 2) {
      _candles.insert(0, candle);
      return;
    }

    final latestInterval = _candles[0].date.difference(_candles[1].date);
    final incomingInterval = candle.date.difference(_candles[0].date);

    if (incomingInterval == latestInterval) {
      _candles.insert(0, candle);
    }
  }

  void _showIntervalDialog(BuildContext context) {
    showDialog<void>(
      context: context,
      builder: (context) {
        return Center(
          child: Material(
            child: Container(
              width: 200,
              color: Theme.of(context).colorScheme.surface,
              child: Wrap(
                children: _intervals
                    .map((interval) => _buildIntervalButton(interval, context))
                    .toList(),
              ),
            ),
          ),
        );
      },
    );
  }

  Widget _buildIntervalButton(String interval, BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: SizedBox(
        width: 50,
        height: 30,
        child: RawMaterialButton(
          elevation: 0,
          fillColor: const Color(0xFF494537),
          onPressed: () {
            if (_currentSymbol.isNotEmpty) {
              _loadCandles(_currentSymbol, interval);
            }

            Navigator.of(context).pop();
          },
          child: Text(
            interval,
            style: const TextStyle(
              color: Color(0xFFF0B90A),
            ),
          ),
        ),
      ),
    );
  }

  void _showSymbolSearchDialog(BuildContext context) {
    showDialog<void>(
      context: context,
      builder: (context) {
        return SymbolsSearchModal(
          symbols: _symbols,
          onSelect: (symbol) {
            _loadCandles(symbol, _currentInterval);
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: _themeIsDark ? ThemeData.dark() : ThemeData.light(),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: StreamBuilder<Object?>(
            stream: _channel?.stream,
            builder: (context, snapshot) {
              WidgetsBinding.instance.addPostFrameCallback((_) {
                if (mounted) {
                  _handleSocketSnapshot(snapshot);
                }
              });

              return Column(
                children: [
                  ToolBar(
                    leftChildren: [
                      ToolBarAction(
                        onPressed: _controller.zoomOut,
                        child: const Icon(Icons.remove),
                      ),
                      ToolBarAction(
                        onPressed: _controller.zoomIn,
                        child: const Icon(Icons.add),
                      ),
                      ToolBarAction(
                        onPressed: () => _showIntervalDialog(context),
                        child: Text(_currentInterval),
                      ),
                      ToolBarAction(
                        width: 100,
                        onPressed: () => _showSymbolSearchDialog(context),
                        child: Text(_currentSymbol),
                      ),
                    ],
                    rightChildren: [
                      ToolBarAction(
                        width: 50,
                        onPressed: _toggleTheme,
                        child: Icon(
                          _themeIsDark
                              ? Icons.wb_sunny_sharp
                              : Icons.nightlight_round_outlined,
                        ),
                      ),
                    ],
                  ),
                  Expanded(
                    child: Candlesticks(
                      key: Key('$_currentSymbol-$_currentInterval'),
                      candles: _candles,
                      onLoadMoreCandles: _loadMoreCandles,
                      controller: _controller,
                    ),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}
136
likes
160
points
1.65k
downloads

Documentation

API reference

Publisher

verified publisherrmzy.dev

Weekly Downloads

A high-performance, interactive Flutter candlestick chart for financial apps.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on candlesticks