art_adk 1.0.2 copy "art_adk: ^1.0.2" to clipboard
art_adk: ^1.0.2 copied to clipboard

Flutter SDK for ART realtime messaging — WebSocket channels, presence tracking, end-to-end encrypted channels, and CRDT-backed shared objects.

example/lib/main.dart

import 'dart:convert';

import 'package:art_adk/art_adk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;

/// ─────────────────────────────────────────────────────────────────────────────
/// 0. Configure these three values for your environment
/// ─────────────────────────────────────────────────────────────────────────────
///
/// • Put your ART credentials JSON in `assets/adk-services.json`.
///   Format documented in the main README.
/// • Replace [kServerUri] with the WebSocket host of your ART deployment.
/// • Replace [kPasscodeEndpoint] with your backend's passcode-mint URL.
/// • Replace [kUsername] / [kChannel] to match your setup.
const String kServerUri = 'YOUR_WEBSOCKET_URI';
const String kPasscodeEndpoint =
    'https://dev.arealtimetech.com/ws/v1/connect/passcode';
const String kUsername = 'USER_NAME';
const String kChannel = 'YOUR_CHANNEL_NAME';

void main() => runApp(const AdkExampleApp());

class AdkExampleApp extends StatelessWidget {
  const AdkExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ART ADK Example',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const AdkHomePage(),
    );
  }
}

/// ─────────────────────────────────────────────────────────────────────────────
/// Home page — walks through every major ADK feature via buttons.
/// ─────────────────────────────────────────────────────────────────────────────
class AdkHomePage extends StatefulWidget {
  const AdkHomePage({super.key});

  @override
  State<AdkHomePage> createState() => _AdkHomePageState();
}

class _AdkHomePageState extends State<AdkHomePage> {
  Adk? _adk;
  BaseSubscription? _subscription;
  final List<String> _log = <String>[];

  LiveObjSubscription? get _liveObj {
    final sub = _subscription;
    return sub is LiveObjSubscription ? sub : null;
  }

  void _logEvent(String message) {
    debugPrint(message);
    if (mounted) {
      setState(() {
        _log.add('[${DateTime.now().toIso8601String().substring(11, 19)}] $message');
      });
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 1. Connect
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _connect() async {
    try {
      _logEvent('connecting...');
      final credentials = await _loadCredentials();
      final passcode = await _fetchPasscode(credentials);

      final updatedCredentials = credentials.copyWith(accessToken: passcode);

      final adk = Adk(
        adkConfig: AdkConfig(
          uri: kServerUri,
          authToken: passcode,
          getCredentials: () => updatedCredentials,
        ),
      );

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

      await adk.connect();
      setState(() => _adk = adk);
    } catch (e) {
      _logEvent('connect failed · $e');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 2. Subscribe to a channel (default, secure, or shared object)
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _subscribe() async {
    final adk = _adk;
    if (adk == null) {
      _logEvent('connect first');
      return;
    }

    try {
      final sub = await adk.subscribe(channel: kChannel);
      setState(() => _subscription = sub);
      _logEvent('subscribed to $kChannel (${sub.channelConfig.channelType})');

      // Bind a named event across any channel type.
      sub.emitter.on('message', (dynamic data) {
        _logEvent('message · $data');
      });

      // On default channels, also stream every event with listen().
      if (sub is Subscription) {
        sub.listen((Map<String, dynamic> data) {
          _logEvent("event=${data['event']}");
        });
      }

      // Track presence (must be enabled on the channel in the dashboard).
      await sub.fetchPresence(
        callback: (List<String> users) => _logEvent('presence · $users'),
      );

      // On CRDT channels, observe the document tree.
      if (sub is LiveObjSubscription) {
        await sub.query(path: 'document').listen((dynamic data) {
          _logEvent('doc · $data');
        });
      }
    }  catch (e) {
      _logEvent('subscribe failed · $e');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 3. Push a message (optionally targeted)
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _sendMessage() async {
    final sub = _subscription;
    if (sub == null) {
      _logEvent('subscribe first');
      return;
    }

    try {
      await sub.push(
        event: 'message',
        data: <String, dynamic>{
          'from': kUsername,
          'text': 'Hello at ${DateTime.now().toIso8601String()}',
        },
        // Required for secure / targeted channels: exactly one recipient.
        // options: const PushConfig(to: <String>['bob']),
      );
      _logEvent('message sent');
    }  catch (e) {
      _logEvent('push failed · $e');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 4. Encryption — generate a keypair once per session
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _generateKeyPair() async {
    final adk = _adk;
    if (adk == null) {
      _logEvent('connect first');
      return;
    }
    try {
      final pair = await adk.generateKeyPair();
      _logEvent('keypair ready · pub=${pair.publicKey.substring(0, 10)}…');
    }  catch (e) {
      _logEvent('keygen failed · $e');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 5. CRDT — set a document title
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _setDocTitle() async {
    final live = _liveObj;
    if (live == null) {
      _logEvent('subscribe to a shared-object channel first');
      return;
    }
    live.state()['document']['title'].set(
      'Title @ ${DateTime.now().millisecondsSinceEpoch}',
    );
    await live.flush();
    _logEvent('title written');
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 6. CRDT — array push / pop
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _pushToArray() async {
    final live = _liveObj;
    if (live == null) return;
    final length = live.state()['items'].push('item ${DateTime.now()}');
    await live.flush();
    _logEvent('items · push (len=$length)');
  }

  Future<void> _popFromArray() async {
    final live = _liveObj;
    if (live == null) return;
    final removed = live.state()['items'].pop();
    await live.flush();
    _logEvent('items · pop ($removed)');
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 7. Interceptor — log every message that passes through
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _addInterceptor() async {
    final adk = _adk;
    if (adk == null) return;
    try {
      await adk.intercept(
        interceptor: 'demo-logger',
        fn:
            (
              Map<String, dynamic> payload,
              void Function(dynamic data) resolve,
              void Function(String error) reject,
            ) {
              _logEvent('intercepted · ${payload['event']}');
              resolve(payload);
            },
      );
      _logEvent('interceptor installed');
    }  catch (e) {
      _logEvent('interceptor failed · $e');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // 8. Teardown
  // ───────────────────────────────────────────────────────────────────────────
  Future<void> _disconnect() async {
    try {
      await _subscription?.unsubscribe();
      await _adk?.disconnect();
    } finally {
      if (mounted) {
        setState(() {
          _subscription = null;
          _adk = null;
        });
      }
      _logEvent('disconnected');
    }
  }

  // ───────────────────────────────────────────────────────────────────────────
  // Helpers
  // ───────────────────────────────────────────────────────────────────────────
  Future<CredentialStore> _loadCredentials() async {
    try {
      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? ?? '',
      );
    } catch (e) {
      throw Exception('Failed to load assets/adk-services.json: $e');
    }
  }

  Future<String> _fetchPasscode(CredentialStore creds) async {
    final response = await http.post(
      Uri.parse(kPasscodeEndpoint),
      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': kUsername,
        'first_name': 'Alice',
        'last_name': 'Example',
      }),
    );

    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw Exception('passcode request failed (${response.statusCode})');
    }

    final decoded = jsonDecode(response.body) as Map<String, dynamic>;
    final data = decoded['data'];
    final passcode = data is Map<String, dynamic>
        ? data['passcode'] as String?
        : decoded['passcode'] as String?;
    if (passcode == null || passcode.isEmpty) {
      throw Exception('passcode missing in response');
    }
    return passcode;
  }



  // ───────────────────────────────────────────────────────────────────────────
  // UI
  // ───────────────────────────────────────────────────────────────────────────
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ART ADK Example'),
        actions: <Widget>[
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Center(
              child: Text(
                _adk?.getState() ?? 'stopped',
                style: Theme.of(context).textTheme.labelMedium,
              ),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: () => setState(() => _log.clear()),
            tooltip: 'Clear Log',
          ),
        ],
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(12),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                _btn('Connect', _connect, primary: true),
                _btn('Subscribe', _subscribe),
                _btn('Generate KeyPair', _generateKeyPair),
                _btn('Send Message', _sendMessage),
                _btn('CRDT · Set Title', _setDocTitle),
                _btn('CRDT · Array Push', _pushToArray),
                _btn('CRDT · Array Pop', _popFromArray),
                _btn('Add Interceptor', _addInterceptor),
                _btn('Disconnect', _disconnect, destructive: true),
              ],
            ),
          ),
          const Divider(height: 1),
          Expanded(
            child: Container(
              color: Colors.black12.withValues(alpha: 0.03),
              child: ListView.builder(
                padding: const EdgeInsets.all(12),
                itemCount: _log.length,
                itemBuilder: (_, int i) => Text(
                  _log[i],
                  style: const TextStyle(fontFamily: 'Menlo', fontSize: 12),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _btn(
    String label,
    Future<void> Function() onTap, {
    bool primary = false,
    bool destructive = false,
  }) {
    final style = primary
        ? FilledButton.styleFrom()
        : destructive
        ? FilledButton.styleFrom(
            backgroundColor: Colors.red.shade400,
            foregroundColor: Colors.white,
          )
        : null;

    return primary || destructive
        ? FilledButton(style: style, onPressed: onTap, child: Text(label))
        : OutlinedButton(onPressed: onTap, child: Text(label));
  }
}
4
likes
160
points
259
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarealtimetech.com

Weekly Downloads

Flutter SDK for ART realtime messaging — WebSocket channels, presence tracking, end-to-end encrypted channels, and CRDT-backed shared objects.

Homepage

Topics

#websocket #realtime #messaging #pubsub #flutter

License

MIT (license)

Dependencies

flutter, http, pinenacl, web_socket_channel

More

Packages that depend on art_adk