pub.dev License: MIT Platform

Flutter SDK for ART — A Realtime Tech communication,, a realtime messaging platform providing WebSocket-based channels, presence tracking, end-to-end encrypted messaging, and CRDT-backed shared objects.

Features

  • WebSocket connection management — connect, pause, resume, and auto-reconnect with exponential backoff
  • Channel subscriptions — default, targeted, group, secure (encrypted), and shared-object (CRDT) channels
  • Push messages — send structured payloads with optional per-user targeting
  • Event listening — receive every message with listen() or bind to named events via emitter.on()
  • Presence tracking — observe users online on a channel in real time
  • End-to-end encryption — crypto applied transparently on secure channels
  • Interceptors — hook into the message pipeline to resolve or reject payloads
  • Shared objects (CRDT) — collaborative state with automatic conflict resolution

Installation

Add art_adk to your pubspec.yaml:

dependencies:
  art_adk: ^1.0.2

Fetch packages:

flutter pub get

Import the SDK where you need it:

import 'package:art_adk/art_adk.dart';

Configuration

1. Create assets/adk-services.json

Generate client credentials from the ART Live Dashboard and save them under assets/adk-services.json:

{
  "Client-ID": "xxxxxxxx",
  "Client-Secret": "xxxxxxxxxxxxxxxx",
  "Org-Title": "YOUR_ORG",
  "ProjectKey": "YOUR_PROJECT_KEY",
  "Environment": "YOUR_ENV"
}

Register the asset in your app's pubspec.yaml:

flutter:
  assets:
    - assets/adk-services.json

See Workspace and Project for how to obtain ProjectKey and Environment.

2. Load the credentials

import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:art_adk/art_adk.dart';

Future<CredentialStore> loadCredentials() async {
  final raw = await rootBundle.loadString('assets/adk-services.json');
  final json = jsonDecode(raw) as Map<String, dynamic>;
  return CredentialStore(
    environment:  json['Environment']   as String,
    projectKey:   json['ProjectKey']    as String,
    orgTitle:     json['Org-Title']     as String,
    clientID:     json['Client-ID']     as String,
    clientSecret: json['Client-Secret'] as String,
  );
}

3. Get a user passcode

The art_adk package authenticates with ART using a short-lived passcode issued for a single user:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> fetchPasscode(CredentialStore creds) async {
  final response = await http.post(
    Uri.parse('https://dev.arealtimetech.com/ws/v1/connect/passcode'),
    headers: <String, String>{
      'Client-Id':     creds.clientID,
      'Client-Secret': creds.clientSecret,
      'X-Org':         creds.orgTitle,
      'Environment':   creds.environment,
      'ProjectKey':    creds.projectKey,
      'Content-Type':  'application/json',
    },
    body: jsonEncode(<String, dynamic>{
      'username':   'jane_doe',
      'first_name': 'Jane',
      'last_name':  'Doe',
    }),
  );

  final decoded = jsonDecode(response.body) as Map<String, dynamic>;
  final data    = decoded['data'] as Map<String, dynamic>;
  return data['passcode'] as String;
}

Quick start

import 'package:art_adk/art_adk.dart';

Future<void> main() async {
  final creds    = await loadCredentials();                 // from asset JSON
  final passcode = await fetchPasscode(creds);              // from your auth service
  final updated  = creds.copyWith(accessToken: passcode);

  final adk = Adk(
    adkConfig: AdkConfig(
      uri:            'ws.arealtimetech.com',
      authToken:      passcode,
      getCredentials: () => updated,
    ),
  );

  adk.on('connection', (dynamic data) {
    if (data is ConnectionDetail) {
      debugPrint('connected → ${data.connectionId}');
    }
  });
  adk.on('close', (dynamic reason) => debugPrint('closed: $reason'));

  await adk.connect();

  final sub = await adk.subscribe(channel: 'room-42');
  sub.emitter.on('message', (dynamic data) {
    debugPrint('received: $data');
  });

  await sub.push(
    event: 'message',
    data:  <String, dynamic>{'text': 'Hello ART!'},
  );
}

Connecting

final adk = Adk(
  adkConfig: AdkConfig(
    uri:            'ws.arealtimetech.com', // scheme optional; wss:// is added automatically
    authToken:      passcode,
    getCredentials: () => updatedCreds,
  ),
);

await adk.connect();

For short-lived workflows, wrap connect / disconnect in try / finally:

try {
await adk.connect();
// your logic here
} finally {
await adk.disconnect();
}

Read the current state at any time:

adk.getState(); // 'connected' | 'retrying' | 'paused' | 'stopped'

Subscribing to a channel

adk.subscribe returns a BaseSubscription. For default / secure / targeted / group channels it is a Subscription; for shared-object channels it is a LiveObjSubscription.

final BaseSubscription sub = await adk.subscribe(channel: 'room-42');

if (sub is LiveObjSubscription) {
  // Shared-object (CRDT) channel — use sub.state(), sub.query(), sub.flush().
}

Unsubscribe when you no longer need the channel:

await sub.unsubscribe();

Pushing messages

await sub.push(
  event: 'message',
  data:  <String, dynamic>{'text': 'Hello'},
);

Target a specific user (required for secure and targeted channels — exactly one recipient):

await sub.push(
  event:   'message',
  data:    <String, dynamic>{'text': 'Hi Bob'},
  options: const PushConfig(to: <String>['bob']),
);

Attempting to target zero or more than one recipient on a secure / targeted channel throws ARTError.serverError.

Receiving messages

Bind to a specific event on the subscription's emitter:

sub.emitter.on('message', (dynamic data) {
  debugPrint('got: $data');
});

Or, on default channels, cast to Subscription and stream every event:

if (sub is Subscription) {
  sub.listen((Map<String, dynamic> data) {
    debugPrint("event=${data['event']} content=${data['content']}");
  });
}

Unbind a specific listener:

if (sub is Subscription) {
  sub.remove(event: 'message');
}

Presence

Track who is online on a channel:

final cancelPresence = await sub.fetchPresence(
  callback: (List<String> users) {
    debugPrint('online: $users');
  },
);

// later:
await cancelPresence();

Presence tracking must be enabled on the channel in the ART dashboard.

Encrypted channels

Secure channels apply end-to-end NaCl-box encryption per message. After connecting, generate (or set) a key pair — the SDK publishes the public half automatically:

await adk.generateKeyPair();          // creates + uploads a fresh KeyPair
// or
await adk.setKeyPair(existingPair);   // adopt an externally-managed KeyPair

Then push as usual. Encryption and decryption happen transparently:

final secure = await adk.subscribe(channel: 'YOUR_SECURE_CHANNEL');

await secure.push(
  event:   'message',
  data:    <String, dynamic>{'text': 'This is private'},
  options: const PushConfig(to: <String>['bob']), // secure requires exactly one recipient
);

secure.emitter.on('message', (dynamic data) {
  debugPrint('decrypted: $data');
});

Interceptors never see encrypted channel traffic — ART preserves message privacy end-to-end.

Shared object channels (CRDT)

Shared-object channels expose a JSON-like document that every subscriber edits concurrently; conflicts are resolved automatically via the SDK's CRDT engine.

final sub = await adk.subscribe(channel: 'YOUR_CRDT_CHANNEL');

if (sub is LiveObjSubscription) {
  // 1. Write
  sub.state()['document']['title'].set('My Doc');
  await sub.flush();

  // 2. Read once
  final snapshot = await sub.query(path: 'document').execute();
  debugPrint('doc: $snapshot');

  // 3. Observe
  final dispose = await sub.query(path: 'document').listen((dynamic data) {
    debugPrint('document updated: $data');
  });

  // later:
  dispose();
}

Array operations

final items = sub.state()['items']; // CRDTProxy

items.push('alpha');                // append
items.unshift('zero');              // prepend
items.pop();                        // remove last
items.removeAt(2);                  // remove by index
items.splice(start: 1, deleteCount: 2, insert: <dynamic>['x', 'y']);

await sub.flush();
debugPrint('length: ${items.length}');

Pending local writes are auto-batched and flushed every 50 ms; call flush() to force an immediate merge.

Interceptors

Interceptors let you plug custom logic into the server-side message pipeline. Each intercepted message must be resolved (forwarded) or rejected.

await adk.intercept(
  interceptor: 'profanity-filter',
  fn: (
    Map<String, dynamic> payload,
    void Function(dynamic data) resolve,
    void Function(String error) reject,
  ) {
    final content = payload['data'];
    if (content is Map && '${content['text']}'.contains('anyword')) {
      reject('blocked by profanity filter');
      return;
    }
    resolve(payload);
  },
);

Encrypted channels are never routed through interceptors.

Connection lifecycle

// Life-cycle listeners
adk.on('connection', (dynamic data) {
if (data is ConnectionDetail) {
debugPrint('connectionId=${data.connectionId}');
}
});
adk.on('close', (dynamic reason) => debugPrint('closed: $reason'));

// Manual controls
adk.pause();               // tear the socket down, keep subscriptions
await adk.resume();        // bring it back
await adk.disconnect();    // full teardown

Reconnection is automatic — up to 5 attempts with a 3–5 s backoff, after which the state becomes stopped and you must call connect() manually.

Common codes: notConnected, authenticationFailed, serverError, encryptionError, decryptionError, timeout, channelNotFound, forbidden.

API reference

Class / type Purpose
Adk Top-level facade — connect, subscribe, intercept, key management
AdkConfig Connection configuration (URI, credentials, auth token)
CredentialStore Immutable holder for org / project / client credentials
BaseSubscription Base type for all channel subscriptions (push, fetchPresence, unsubscribe)
Subscription Default / secure / targeted / group channel — listen(), bind(), emitter.on()
LiveObjSubscription CRDT-backed shared-object channel — state(), query(), flush()
CRDTProxy Chainable accessor for reading / mutating CRDT state
PushConfig Push options — to: List<String> for targeted delivery
KeyPairType KeyPairType for secure channels
ConnectionDetail Emitted on 'connection' — contains connectionId, instanceId, tenant info

Documentation

Full documentation is available at docs.arealtimetech.com/docs/adk.

Topic Link
Overview ADK Overview
Installation Flutter Installation
Publish & Subscribe Pub/Sub Docs
Connection Management Connection Docs
User Presence Presence Docs
Encrypted Channels Encryption Docs
Shared Object Channels Shared Object Docs
Interceptors Interceptor Docs

License

Released under the MIT License.

Libraries

art_adk
Flutter SDK for ART.