art_adk 1.0.2
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));
}
}