NeoCharts
Quick Start

Android Integration

Embed NeoCharts into a native Android app via platform channels.

Embed NeoCharts into an existing native Android app (Kotlin / Java).

The SDK is distributed as a pre-built binary (AAR) — your native app only implements the platform channel handlers that supply data and receive trading callbacks.

This is the complete integration — the full channel set, alerts, OI analysis, and scalper mode. Coming from Quick Start? Steps 1–3 below are the same ones you just ran.

Android integration architecture


Requirements

RequirementMinimum
Android API level24 (Android 7.0)
Kotlin1.7+
Android Gradle Plugin8.x
Java17

1. Add the provided .aar files

Access

NeoCharts is distributed directly to licensed partners, not published to a public repository. Your IOURING contact sends you the .aar file(s) directly — no repository involved. Place them in a local libs/ folder in your app module.

// app/build.gradle.kts
dependencies {
    releaseImplementation(files("libs/nxtchart-flutter_release.aar"))
    // additional plugin AARs provided alongside it, referenced the same way
}

2. Register channel handlers

Register all handlers in Application.onCreate before the Flutter engine starts.

import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
import org.json.JSONObject

class App : Application() {

    // EventChannel sinks — whichever view is active holds the live reference
    var marketDataSink: EventChannel.EventSink? = null
    var ordersSink:     EventChannel.EventSink? = null
    var ocoOrdersSink:  EventChannel.EventSink? = null
    var positionsSink:  EventChannel.EventSink? = null
    var tradeEventSink: EventChannel.EventSink? = null
    var alertsSink:     EventChannel.EventSink? = null

    override fun onCreate() {
        super.onCreate()

        val group = FlutterEngineGroup(this)
        val chartEngine = group.createAndRunEngine(
            this, DartExecutor.DartEntrypoint.createDefault()
        )
        val scalperEngine = group.createAndRunEngine(
            this,
            DartExecutor.DartEntrypoint(
                FlutterInjector.instance().flutterLoader().findAppBundlePath(),
                "scalperMain",
            ),
        )
        FlutterEngineCache.getInstance().put("nxtchart_chart", chartEngine)
        FlutterEngineCache.getInstance().put("nxtchart_scalper", scalperEngine)

        registerChannels(chartEngine.dartExecutor.binaryMessenger)
        registerChannels(scalperEngine.dartExecutor.binaryMessenger)
    }

    private fun registerChannels(messenger: BinaryMessenger) {

        // ── nxtchart/data ─────────────────────────────────────────────────────
        MethodChannel(messenger, "nxtchart/data")
            .setMethodCallHandler { call, result ->
                when (call.method) {

                    // ── Init metadata (called once at startup) ───────────────
                    "symbolInfo"           -> result.success(yourSymbolInfoJson())
                    "underlyingSymbolInfo" -> result.success(yourUnderlyingSymbolInfo())
                    "optionSymbols"        -> result.success(yourOptionSymbolsJson())
                    "futureSymbols"        -> result.success(yourFutureSymbolsJson())
                    // Populating this hides the Volume indicator for symbols
                    // in the list (spot indices have no traded volume).
                    "indexSymbols"         -> result.success(yourIndexSymbolsJson())
                    "marketTiming"         -> result.success(yourMarketTimingJson())
                    "hasOCO"               -> result.success(true)
                    "storageKey"           -> result.success("user_${yourUserId()}")
                    // Called each time the scalper right panel initializes, not
                    // cached at startup — safe to delay until background data
                    // (e.g. option chain) finishes loading.
                    "atmSymbols"           -> result.success(yourAtmSymbolsJson())
                    "chartTopOptions"      -> fetchTopOptions { json -> result.success(json) }

                    // ── Historical data ──────────────────────────────────────
                    "loadData" -> {
                        val from            = call.argument<Int>("from")!!.toLong()
                        val to              = call.argument<Int>("to")!!.toLong()
                        val intervalSeconds = call.argument<Int>("intervalSeconds")!!
                        val requiredBars    = call.argument<Int>("requiredBars")!!
                        fetchOHLCV(from, to, intervalSeconds, requiredBars) { json ->
                            result.success(json)
                        }
                    }

                    // ── Option chain & OI analysis ────────────────────────────
                    "fetchOptionDetails" -> {
                        val underlyingSymbolId = call.argument<String>("underlyingSymbolId")!!
                        fetchOptionDetails(underlyingSymbolId) { json -> result.success(json) }
                    }
                    "fetchOIAnalysis" -> {
                        val underlyingSymbolId = call.argument<String>("underlyingSymbolId")!!
                        val expiry             = call.argument<String>("expiry")!!
                        val timeFrom           = call.argument<Int>("timeFrom")!!
                        val timeTo             = call.argument<Int>("timeTo")!!
                        fetchOIAnalysis(underlyingSymbolId, expiry, timeFrom, timeTo) { json ->
                            result.success(json)
                        }
                    }
                    "fetchOIChange" -> {
                        val underlyingSymbolId = call.argument<String>("underlyingSymbolId")!!
                        val expiries           = call.argument<List<String>>("expiries")!!
                        val timeFrom           = call.argument<Int>("timeFrom")!!
                        val timeTo             = call.argument<Int>("timeTo")!!
                        fetchOIChange(underlyingSymbolId, expiries, timeFrom, timeTo) { json ->
                            result.success(json)
                        }
                    }
                    "fetchOI" -> {
                        val underlyingSymbolId = call.argument<String>("underlyingSymbolId")!!
                        val expiries           = call.argument<List<String>>("expiries")!!
                        fetchOI(underlyingSymbolId, expiries) { json -> result.success(json) }
                    }
                    "fetchPcrIntraday"         -> fetchPcrIntraday { json -> result.success(json) }
                    "fetchAtmStraddleIntraday" -> fetchAtmStraddleIntraday { json -> result.success(json) }
                    "fetchAtmIvIntraday"       -> fetchAtmIvIntraday { json -> result.success(json) }

                    // ── Alerts ─────────────────────────────────────────────────
                    "modifyAlert" -> {
                        yourBroker.modifyAlert(call.arguments as String)
                        result.success(null)
                    }
                    "createAlert" -> {
                        yourBroker.createAlert(call.arguments as String)
                        result.success(null)
                    }
                    "deleteAlert" -> {
                        val alertId = call.argument<String>("alertId")!!
                        yourBroker.deleteAlert(alertId)
                        result.success(null)
                    }

                    // ── Order actions (params arrive as a raw JSON string —
                    //    see Reference for each method's exact shape) ──────────
                    "placeOrder" -> {
                        val p = JSONObject(call.arguments as String)
                        yourBroker.placeOrder(
                            p.getString("symID"), p.getDouble("price"),
                            p.getString("orderType"), p.getString("orderAction"),
                            p.getInt("qty"),
                        )
                        result.success(null)
                    }
                    "modifyOrder" -> {
                        val p = JSONObject(call.arguments as String)
                        yourBroker.modifyOrder(
                            p.getString("orderID"), p.getString("symID"), p.getDouble("price"),
                            p.getString("orderType"), p.getString("orderAction"), p.getInt("qty"),
                        )
                        result.success(null)
                    }
                    "cancelOrder" -> {
                        val orderID = call.argument<String>("orderID")!!
                        yourBroker.cancelOrder(orderID)
                        result.success(null)
                    }
                    "placeOCOOrder" -> {
                        yourBroker.placeOCOOrder(call.arguments as String)
                        result.success(null)
                    }
                    "modifyOCOOrder" -> {
                        yourBroker.modifyOCOOrder(call.arguments as String)
                        result.success(null)
                    }
                    "cancelOCOOrder" -> {
                        val groupId = call.argument<String>("groupId")!!
                        yourBroker.cancelOCOOrder(groupId)
                        result.success(null)
                    }
                    "groupAdjustOrders" -> {
                        yourBroker.groupAdjustOrders(call.arguments as String)
                        result.success(null)
                    }

                    else -> result.notImplemented()
                }
            }

        // ── nxtchart/marketData ───────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/marketData")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    marketDataSink = sink
                    val symbols = args as? String
                    yourTickSource.subscribe(symbols) { tickJson ->
                        marketDataSink?.success(tickJson)
                    }
                }
                override fun onCancel(args: Any?) { marketDataSink = null }
            })

        // ── nxtchart/symbolSearch ─────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/symbolSearch")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    // The chart takes only the first emission per query —
                    // no need to keep pushing after the initial result.
                    val query = args as? String
                    yourApi.searchSymbols(query) { json -> sink.success(json) }
                }
                override fun onCancel(args: Any?) {}
            })

        // ── nxtchart/orders ───────────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/orders")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    ordersSink = sink
                }
                override fun onCancel(args: Any?) { ordersSink = null }
            })

        // ── nxtchart/ocoOrders ────────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/ocoOrders")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    ocoOrdersSink = sink
                }
                override fun onCancel(args: Any?) { ocoOrdersSink = null }
            })

        // ── nxtchart/positions ────────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/positions")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    positionsSink = sink
                }
                override fun onCancel(args: Any?) { positionsSink = null }
            })

        // ── nxtchart/tradeEvents ──────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/tradeEvents")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    tradeEventSink = sink
                }
                override fun onCancel(args: Any?) { tradeEventSink = null }
            })

        // ── nxtchart/alerts ───────────────────────────────────────────────────
        EventChannel(messenger, "nxtchart/alerts")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(args: Any?, sink: EventChannel.EventSink) {
                    alertsSink = sink
                }
                override fun onCancel(args: Any?) { alertsSink = null }
            })
    }
}

3. Launching the chart

// Single chart view
startActivity(FlutterActivity.withCachedEngine("nxtchart_chart").build(this))

// Scalper view (multi-panel, opens in landscape)
startActivity(FlutterActivity.withCachedEngine("nxtchart_scalper").build(this))

Or embed as a Fragment:

val chartFragment = FlutterFragment
    .withCachedEngine("nxtchart_chart")
    .build()

supportFragmentManager
    .beginTransaction()
    .add(R.id.chart_container, chartFragment, "chart")
    .commit()

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


4. Pushing live data

Once the EventChannel sinks are active, push data from wherever your streams emit:

val app = application as App

// Live ticks
app.marketDataSink?.success(
    """[{"symbolId":"RELIANCE","ltp":2411.5,"ltq":100,"chng":11.5,"chngPer":0.48,"ltt":1700001234}]"""
)

// Updated orders list — see Reference for the full Order JSON schema
app.ordersSink?.success(
    """[{"orderID":"ORD123","type":"limit","orderAction":"buy","productType":"normal","avgPrice":2400.0,"netQty":10,"fillQty":0,"ordTime":"2025-11-20T09:15:00","orderStatus":"open","symbol":{"id":"RELIANCE","...":"see symbolInfo"}}]"""
)

// Updated OCO orders list (pending SL+TP pairs) — see Reference for the full schema
app.ocoOrdersSink?.success(
    """[{"groupId":"OCO001","symID":"RELIANCE","name":"Reliance Industries","exchange":"NSE","side":"buy","productType":"normal","stopLoss":{"type":"stopLoss","side":"sell","triggerPrice":2350.0,"qty":10,"price":2340.0,"fillQty":0},"target":{"type":"limit","side":"sell","triggerPrice":2450.0,"qty":10,"price":2450.0,"fillQty":0}}]"""
)

// Updated positions list — see Reference for the full Position JSON schema
app.positionsSink?.success(
    """[{"symID":"RELIANCE","netQty":10,"avgPrice":2400.0,"pnl":115.0,"productType":"normal"}]"""
)

// Action feedback (order/alert outcome notification)
app.tradeEventSink?.success(
    """{"type":"positive","message":"Order placed successfully"}"""
)

// Updated alerts list — see Reference for the full alert JSON schema
app.alertsSink?.success(
    """[{"alertId":"357807025062912","symbolInfo":{"id":"RELIANCE","...":"see symbolInfo"},"triggerPrice":"2450","enabled":true,"triggered":false,"createdAt":1769509922000}]"""
)

Tip

Push ordersSink and positionsSink before or during loadData so the chart has order/position state ready on first render.


5. AndroidManifest.xml

<activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:enableOnBackInvokedCallback="false"
    android:exported="false"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize" />

<uses-permission android:name="android.permission.INTERNET" />

Required, not optional

android:enableOnBackInvokedCallback="false" — set it explicitly even though it defaults to false on paper. NxtChartPage closes overlays (order pad, popups, bottom sheets) on back press instead of exiting the chart; Android's predictive-back gesture can race that behavior and exit the whole activity instead. Setting this flag disables predictive-back dispatch on the host <activity> and removes the race.


Channel Reference

See the Native Channel Reference for the full channel list and nxtchart/data method table — identical on Android and iOS, only the calling syntax differs (shown above).


Troubleshooting

SymptomFix
minSdkVersion build errorSet minSdk = 24 in build.gradle.kts
Blank chart — init never completesEnsure all 9 init methods on nxtchart/data return a value (no notImplemented)
Blank chart — no candlesVerify loadData returns valid OHLCV JSON ordered oldest-first
Orders/positions not shownPush to ordersSink/positionsSink before or during loadData
Alerts not shownPush to alertsSink whenever your alert list changes
MissingPluginExceptionRegister all channels in Application.onCreate before engine starts
Back button exits the whole chart/app instead of closing an overlaySet android:enableOnBackInvokedCallback="false" on the host <activity> — see § 5 above

See the API Reference for all channel method details and JSON Schemas for wire formats. See How It Works for the data and trade flow.

On this page