NeoCharts
Quick Start

Flutter Integration

Integrate NeoCharts into a Flutter app by implementing ChartInterface and mounting NxtChartPage.

Drop in one widget, implement one interface, get a fully interactive chart with real-time data, technical indicators, order/position overlays, and drawing tools.

This is the complete integration — trading, alerts, option chain, and scalper mode. Coming from Quick Start? Steps 1–3 below are the same ones you just ran.

Flutter integration architecture


1. Add the dependency

Access

NeoCharts is distributed directly to licensed partners, not published to pub.dev. Your IOURING contact provides the Git repository URL and version tag below.

# pubspec.yaml
dependencies:
  nxtchart:
    git:
      url: <repository-url>
      ref: <version-tag>

Pick ref: from the repository's git tags (e.g. nxtchart-v0.0.35+17) — don't invent one.

fvm flutter pub get

2. Implement ChartInterface

import 'package:nxtchart/interface.dart';

class MyChartController implements ChartInterface {
  @override
  String get storageKey => 'user_123'; // stable, user-scoped key

  @override
  String get symbolInfo => jsonEncode({
    'id': 'RELIANCE',
    'name': 'Reliance Industries',
    'lotSize': 1,
    'precision': 2,
    'tickSize': 0.05,
    'exchange': 'NSE',
    'expiry': '',
    'strike': '',
    'optType': '',
    'weekly': '',
    'undID': '',
  });

  @override
  String? get underlyingSymbolInfo => null; // null for equities

  @override
  String get optionSymbols => '[]';

  @override
  String? get futureSymbols => null;

  @override
  String? get indexSymbols => null;

  @override
  Future<String?> get atmSymbols async => null;

  @override
  String get marketTiming => jsonEncode({
    'timezone': 'Asia/Kolkata',
    'sessions': [
      ['0915-1530'], // Monday
      ['0915-1530'], // Tuesday
      ['0915-1530'], // Wednesday
      ['0915-1530'], // Thursday
      ['0915-1530'], // Friday
      <String>[], // Saturday
      <String>[], // Sunday
    ],
    'holidays': <String>[],
    'special': <String, dynamic>{},
  });

  @override
  Future<String> get chartTopOptions async {
    // Return JSON list of top options ranked by volume (same shape as optionSymbols)
    return yourApi.fetchTopOptions();
  }

  @override
  Future<String> loadData({
    required String symbolId,
    required int from,
    required int to,
    required int intervalSeconds,
    required int requiredBars,
  }) async {
    final bars = await yourApi.fetchOHLCV(
      symbolId: symbolId,
      from: from,
      to: to,
      interval: intervalSeconds,
    );
    return jsonEncode(bars);
  }

  @override
  Stream<String> marketDataStreamer(String symbols) {
    return yourTickStream.map((ticks) => jsonEncode(ticks));
  }

  @override
  Stream<String> searchSymbolsStreamer(String query) {
    return yourApi.searchSymbols(query).asStream().map(jsonEncode);
  }

  @override
  // Called when the user taps a related/underlying symbol on screen (e.g. an
  // option's underlying). Return a ChartInterface for that symbol to enable
  // in-chart navigation, or null to disable it.
  ChartInterface? chartInterfaceForSymbol(String symbolJson) => null;

  @override
  bool get hasOCO => true;

  @override
  void placeOrder(String params) {
    yourBroker.placeOrder(jsonDecode(params));
  }

  @override
  void modifyOrder(String params) => yourBroker.modifyOrder(jsonDecode(params));

  @override
  void cancelOrder(String orderID) => yourBroker.cancelOrder(orderID);

  @override
  void placeOCOOrder(String params) =>
      yourBroker.placeOCOOrder(jsonDecode(params));

  @override
  void modifyOCOOrder(String params) =>
      yourBroker.modifyOCOOrder(jsonDecode(params));

  @override
  void cancelOCOOrder(String groupId) => yourBroker.cancelOCOOrder(groupId);

  @override
  void groupAdjustOrders(String params) =>
      yourBroker.groupAdjustOrders(jsonDecode(params));

  @override
  Stream<String> get ordersStreamer => yourOrdersStream;

  @override
  Stream<String> get positionsStreamer => yourPositionsStream;

  @override
  Stream<String> get actionFeedbackStreamer =>
      yourActionFeedbackStream.asBroadcastStream();

  @override
  Stream<String> get ocoOrdersStreamer => yourOcoOrdersStream;

  @override
  Future<String> fetchOptionDetails({required String underlyingSymbolId}) =>
      yourApi.fetchOptionChain(underlyingSymbolId);

  @override
  Future<String?> fetchOIAnalysis({
    required String underlyingSymbolId,
    required String expiry,
    required int timeFrom,
    required int timeTo,
  }) =>
      yourApi.fetchOIAnalysis(underlyingSymbolId, expiry, timeFrom, timeTo);

  @override
  Future<String?> fetchOIChange({
    required String underlyingSymbolId,
    required List<String> expiries,
    required int timeFrom,
    required int timeTo,
  }) =>
      yourApi.fetchOIChange(underlyingSymbolId, expiries, timeFrom, timeTo);

  @override
  Future<String?> fetchOI({
    required String underlyingSymbolId,
    required List<String> expiries,
  }) =>
      yourApi.fetchOI(underlyingSymbolId, expiries);

  @override
  Future<String> fetchPcrIntraday() => yourApi.fetchPcrIntraday();

  @override
  Future<String> fetchAtmStraddleIntraday() =>
      yourApi.fetchAtmStraddleIntraday();

  @override
  Future<String> fetchAtmIvIntraday() => yourApi.fetchAtmIvIntraday();

  @override
  void modifyAlert(String params) => yourBroker.modifyAlert(jsonDecode(params));

  @override
  void createAlert(String params) => yourBroker.createAlert(jsonDecode(params));

  @override
  void deleteAlert(String alertId) => yourBroker.deleteAlert(alertId);

  @override
  Stream<String> get alertsStreamer => yourAlertsStream;

  @override
  void dispose() {
    // close streams, cancel subscriptions
  }
}

Must be broadcast streams

ordersStreamer, positionsStreamer, ocoOrdersStreamer, and actionFeedbackStreamer must be broadcast streams (.asBroadcastStream() or a broadcast StreamController) — a single-subscription stream crashes with Bad state: Stream has already been listened to.


3. Mount the widget

import 'package:nxtchart/widgets.dart';

NxtChartPage(dataProvider: MyChartController())

Theming

Theming reads your app's Material Theme — the chart uses colorScheme.primary/onPrimary for its accent color and follows light/dark mode. Requires a Material Theme ancestor with those set; Cupertino-only apps or an unset color scheme won't get sensible theming for free.

Run the app — you should see a live chart with real-time data, indicators, drawing tools, and working order/position/alert actions.


Scalper mode

Scalper opens the chart in landscape with two side-by-side panels:

  • Left panel — main price chart for the primary symbol
  • Right panel — ATM CE/PE strike chart (requires atmSymbols; return null to leave blank)
@override
Future<String?> get atmSymbols async =>
    jsonEncode(['NIFTY24DEC24000CE', 'NIFTY24DEC24000PE']);

Mount with the .scalper constructor instead of the standard one — no additional Dart code needed. Android hosts do need one manifest flag; see below.

NxtChartPage.scalper(dataProvider: MyChartController())

Required, not optional

Android: the host activity must set android:enableOnBackInvokedCallback="false" — see Android integration § 5 for why. Without it, the system back gesture can exit the whole activity instead of closing an open chart overlay.


Troubleshooting

SymptomFix
Back button exits the whole chart/app instead of closing an overlay (Android)Set android:enableOnBackInvokedCallback="false" on the host <activity> — see android § 5

"Bad state: Stream has already been listened to"

Your streamer is a single-subscription stream. Wrap it:

@override
Stream<String> get actionFeedbackStreamer =>
    _myController.stream.asBroadcastStream();

Or create the controller as broadcast from the start:

final _controller = StreamController<String>.broadcast();

This affects actionFeedbackStreamer, ordersStreamer, positionsStreamer, and ocoOrdersStreamer.

loadData never completes / chart stays blank

Check that your loadData implementation is async and actually awaits its data source:

@override
Future<String> loadData({...}) async {
  final bars = await yourApi.fetchOHLCV(...); // must await
  return jsonEncode(bars);
}

A missing await returns an empty future immediately, which the SDK interprets as an empty dataset.

Chart state lost after navigation

Call dispose() on your ChartInterface implementation when the host widget is removed from the tree:

@override
void dispose() {
  _chartController.dispose(); // your ChartInterface instance
  super.dispose();
}

Without this, the chart engine holds subscriptions open and accumulated state may be replayed on the next mount.


See the API Reference for all ChartInterface members and JSON Schemas for wire formats.

On this page