art_adk 0.0.1
art_adk: ^0.0.1 copied to clipboard
A Flutter SDK for ARealtimeTech (ART) — real-time WebSocket messaging, channel subscriptions, presence, encrypted channels, and CRDT shared objects.
example/lib/main.dart
import 'dart:developer';
import 'package:art_adk/art_adk.dart';
import 'package:example/ui_message.dart';
import 'package:flutter/material.dart';
import 'auth/app_auth_config.dart';
import 'auth/auth_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Adk? adk;
bool isLoading = false;
bool pulse = false;
bool isSubscribed = false;
bool isSubscribing = false;
bool isConnected = false;
String messageText = '';
String clientID = '';
String toUser = 'kiran';
String channelName = 'Flutter-ADK-TAG';
String authUsername = 'yuresh';
BaseSubscription? activeSubscription;
final List<UIMessage> messages = <UIMessage>[];
LiveObjSubscription? get liveObjSubscription {
final subscription = activeSubscription;
if (subscription is LiveObjSubscription) {
return subscription;
}
return null;
}
List<String> interceptors = <String>[];
String interceptorInput = '';
Map<String, Offset> userPositions = <String, Offset>{};
@override
void initState() {
super.initState();
}
Future<void> connect() async {
isLoading = true;
try {
final clientCreds = await AppConfigLoader.loadCredentials();
final passcode = await AuthService.connectWithPasscode(
username: authUsername,
firstName: authUsername,
lastName: authUsername,
credentials: clientCreds,
);
var updatedCreds = clientCreds.copyWith(accessToken: passcode);
final config = AdkConfig(
uri: 'dev.arealtimetech.com/ws',
authToken: passcode,
getCredentials: () => updatedCreds,
);
final instance = Adk(adkConfig: config);
adk = instance;
instance.on('connection', (dynamic data) {
isConnected = true;
pulse = true;
log('✅ Connected');
});
instance.on('close', (dynamic _) {
log('❌ Connection closed');
isConnected = false;
pulse = false;
});
await instance.connect();
} catch (error) {
log('Connection error: $error');
} finally {
isLoading = false;
}
}
Future<void> generateKeyPair() async {
try {
await adk?.generateKeyPair();
log('🔐 Key pair generated');
} catch (error) {
log('error generateKeyPair: $error');
} finally {
}
}
Future<void> subscribe() async {
final currentAdk = adk;
if (currentAdk == null) {
return;
}
isSubscribing = true;
try {
log('Subscribing to $channelName');
final sub = await currentAdk.subscribe(channel: channelName);
activeSubscription = sub;
isSubscribed = true;
isSubscribing = false;
log('✅ Subscribed to $channelName');
await fetchPresence(sub);
if (sub is LiveObjSubscription) {
log('📊 CRDT-enabled subscription');
await listenCRDT();
} else {
log('🔒 Secure/Normal subscription');
}
sub.emitter.on('message', (dynamic data) {
if (data is Map<String, dynamic> && data['message'] is String) {
messages.add(
UIMessage(
id: DateTime.now().microsecondsSinceEpoch.toString(),
sender: toUser,
text: data['message'] as String,
isMe: false,
timestamp: DateTime.now(),
),
);
}
});
} catch (error) {
log('Subscribe error: $error');
isSubscribing = false;
}
}
Future<void> fetchPresence(BaseSubscription sub) async {
try {
await sub.fetchPresence(
callback: (List<String> users) {
log('👥 Online users: $users');
},
);
} catch (error) {
log('❌ Presence error: $error');
}
}
Future<void> sendMessage() async {
if (messageText.isEmpty) {
return;
}
final text = messageText;
messageText = '';
await activeSubscription?.push(
event: 'message',
data: <String, dynamic>{'message': text},
options: PushConfig(to: <String>[toUser]),
);
messages.add(
UIMessage(
id: DateTime.now().microsecondsSinceEpoch.toString(),
sender: authUsername,
text: text,
isMe: true,
timestamp: DateTime.now(),
),
);
}
Future<void> listenCRDT() async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
log('⚠️ CRDT not available - using secure/normal channel');
return;
}
final query = liveSub.query(path: 'user');
final initial = await query.execute();
log('📊 Initial CRDT: $initial');
await query.listen((dynamic data) {
log('🔄 CRDT update: $data');
if (data == null) {
userPositions = <String, Offset>{};
return;
}
if (data is! Map) {
log('❌ Invalid data format: ${data.runtimeType}');
return;
}
double? extractCoordinate(dynamic value) {
if (value is num) {
return value.toDouble();
}
if (value is Map) {
final directValue = value['value'];
if (directValue is num) {
return directValue.toDouble();
}
final nestedValue = value['index'];
if (nestedValue is Map) {
final inner = nestedValue['value'];
if (inner is num) {
return inner.toDouble();
}
}
}
return null;
}
final result = <String, Offset>{};
for (final MapEntry<dynamic, dynamic> entry in data.entries) {
final username = '${entry.key}';
final userData = entry.value;
if (userData is! Map) {
continue;
}
final index = userData['index'];
if (index is! Map) {
log('⚠️ Missing index for $username');
continue;
}
final x = extractCoordinate(index['x']);
final y = extractCoordinate(index['y']);
if (x != null && y != null) {
result[username] = Offset(x, y);
log('✅ Parsed $username: ($x, $y)');
} else {
log('⚠️ Missing x or y for $username. index=$index');
}
}
userPositions = result;
log('📍 Updated ${result.length} user positions');
});
}
Future<void> updateMyPosition({required double x, required double y}) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
log('⚠️ CRDT not available for this channel type');
return;
}
userPositions = <String, Offset>{
...userPositions,
authUsername: Offset(x, y),
};
liveSub.state()['user'][authUsername].set(<String, dynamic>{
'index': <String, dynamic>{'x': x, 'y': y},
});
await liveSub.flush();
log('✅ Position sent successfully');
}
Future<void> addInterceptor() async {
final name = interceptorInput.trim();
if (name.isEmpty) {
return;
}
final currentAdk = adk;
if (currentAdk == null) {
log('⚠️ Please authenticate first');
return;
}
try {
await currentAdk.intercept(
interceptor: name,
fn:
(
Map<String, dynamic> data,
void Function(dynamic data) resolve,
void Function(String error) reject,
) {
log("🔀 Intercepted on '$name': $data");
resolve(data);
},
);
interceptors = <String>[...interceptors, name];
interceptorInput = '';
log('✅ Interceptor added: $name');
} catch (error) {
log('❌ Failed to add interceptor: $error');
} finally {
}
}
void removeInterceptor({required int index}) {
if (index < 0 || index >= interceptors.length) {
return;
}
final removed = interceptors[index];
interceptors = <String>[
...interceptors.take(index),
...interceptors.skip(index + 1),
];
log('🗑 Removed interceptor: $removed');
}
Future<void> crdtArrayPush(String item) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
return;
}
final count = liveSub.state()['items'].push(item);
await liveSub.flush();
log("➕ pushed '$item', count=$count");
}
Future<void> crdtArrayPop() async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
return;
}
final removed = liveSub.state()['items'].pop();
await liveSub.flush();
log('📤 popped: $removed');
}
Future<void> setCRDTTitle(String title) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
log('⚠️ Not a CRDT channel');
return;
}
liveSub.state()['document']['title'].set(title);
await liveSub.flush();
log('✏️ title set → $title');
}
Future<void> disconnect() async {
await activeSubscription?.unsubscribe();
await adk?.disconnect();
activeSubscription = null;
isConnected = false;
isSubscribed = false;
pulse = false;
messages.clear();
userPositions = <String, Offset>{};
log('Disconnected');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("ADK Example")),
body: Center(
child: Column(crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(height: 5,),
ElevatedButton(onPressed: connect, child: Text("Connect to ADK")),
SizedBox(height: 5,),
ElevatedButton(onPressed: sendMessage, child: Text("Send Message")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () => updateMyPosition(x:100,y: 200),
child: Text("Update My Position")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () => setCRDTTitle('My Doc'), child: Text("Set Title")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () =>crdtArrayPush('Hello ${DateTime.now()}'), child: Text("Array Push")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: crdtArrayPop, child: Text("Array Pop")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: disconnect, child: Text("Disconnect")),
],
),
),
),
);
}
}